需求

  • 项目里有个场景,读的并发很大,但仅仅涉及到很少的写操作。那么此时采用读写锁就是个不错的选择,但是默认的读写锁是非公平锁,如果读并发很大,写请求到达时可能抢不到锁,所以此时需要使用公平读写锁,为了提升复用性,采取注解的形式进行封装。
  • 如果一个class的object需要这个能力,加入注解@ReadWriteResource
  • 具体的函数,如果加@ReadOnlyOperator注解,那么表明这是一个读任务,可以并发执行;
  • 如果加@WriteOperator,那么表明这是一个写任务,和读任务、写任务互斥执行;
  • 需要注意的是,这里需要控制锁粒度为对象级别的锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 伪代码如下
@ReadWriteResource
class ManagedResource {

@ReadOnlyOperator
String getResource(int key);

@ReadOnlyOperator
int getName(int key);

@WriteOperator
void updateResource(int key, String resource);
}

具体实现

  • 采用注解+动态代理的方式来实现,思路就是根据@ReadWriteResource判断该对象是否需要动态代理,然后拦截对象中的方法,在方法执行前后加锁释放。

    1. 首先创建三个注解
    1
    2
    3
    4
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface ReadWriteResource {
    }
    1
    2
    3
    4
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ReadOnlyOperator {
    }
    1
    2
    3
    4
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WriteOperator {
    }
    1. 然后编写一个代理工厂类,用于创建代理对象,首先判断对象的Class是否有@ReadWriteResource注解,以此判断是否要为该对象创建代理对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ReadWriteResourceProxyFactory<T> {

    public T createProxy(T target) {
    Class<?> clazz = target.getClass();
    // 判断该类是否有ReadWriteResource注解
    ReadWriteResource annotation = clazz.getAnnotation(ReadWriteResource.class);
    // 如果有注解,则创建代理对象
    if (annotation != null) {
    // 这里并不需要将target对象传入,因为拦截器拦截的是目标对象的方法,而不是目标对象本身
    ReadWriteResourceInterceptor interceptor = new ReadWriteResourceInterceptor();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(interceptor);
    return (T) enhancer.create();
    }
    // 如果没有注解,直接返回原对象
    return target;
    }
    }
    1. 构造函数先为对象初始化读写锁,拦截逻辑中判断方法上是否存在@ReadOnlyOperatorWriteOperator注解,来确定是读操作还是写操作,然后加上对应的锁。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public class ReadWriteResourceInterceptor implements MethodInterceptor {
    private final Lock readLock;
    private final Lock writeLock;

    public ReadWriteResourceInterceptor() {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    this.readLock = readWriteLock.readLock();
    this.writeLock = readWriteLock.writeLock();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    ReadOnlyOperator readOnlyOperatorAnnotation = method.getAnnotation(ReadOnlyOperator.class);
    WriteOperator writeOperatorAnnotation = method.getAnnotation(WriteOperator.class);

    if (readOnlyOperatorAnnotation != null) {
    // 如果包含ReadOnlyOperator注解,加读锁
    readLock.lock();
    try {
    // 执行原方法
    return methodProxy.invokeSuper(o, objects);
    } finally {
    // 释放读锁
    readLock.unlock();
    }
    } else if (writeOperatorAnnotation != null) {
    // 如果包含WriteOperator注解,加写锁
    writeLock.lock();
    try {
    // 执行原方法
    return methodProxy.invokeSuper(o, objects);
    } finally {
    // 释放写锁
    writeLock.unlock();
    }
    } else {
    // 没加注解的普通方法,返回直接调用原方法
    return methodProxy.invokeSuper(o, objects);
    }
    }
    }
    1. 基本使用,伪代码如下,测试的时候可以在日志中打印出读写锁地址
    1
    2
    3
    4
    ReadWriteResourceProxyFactory<ManagedResource> proxyFactory = new ReadWriteResourceProxyFactory<>();
    ManagedResource proxy = proxyFactory.createProxy(new ManagedResource());
    proxy.getName();
    proxy.updateResource();
    • 写完了打包上传至maven私服,以后就是封装自己的工具类了

AOP实现

  • 之前其实采用的是注解+AOP来封装的,然后写着写着就发现了点问题,这里也来和大家分享一下吧。
  • 首先注解方面没有变化,不过我没用到@ReadWriteResource注解,因为找切点的时候可以直接根据方法上是否有@ReadOnlyOperator@WriteOperator来判断。
  • AOP的逻辑如下
1
// 明天更