Java基于注解实现的锁实例解析

背景

某些场景下,有可能一个方法不能被并发执行,有可能一个方法的特定参数不能被并发执行。比如不能将一个消息发送多次,创建缓存最好只创建一次等等。为了实现上面的目标我们就需要采用同步机制来完成,但同步的逻辑如何实现呢,是否会影响到原有逻辑呢?

嵌入式

这里讲的嵌入式是说获取锁以及释放锁的逻辑与业务代码耦合在一起,又分分布式与单机两种不同场景的不同实现。

单机版本

下面方法,每个productId不允许并发访问,所以这里可以直接用synchronized来锁定不同的参数。

@Service
public class ProductAppService {

  public void invoke(Integer productId) {
    synchronized (productId) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.print("productId:" + productId+" time:"+new Date());
    }
  }
}

测试脚本:三个相同的参数0,两个不同的参数1和2,通过一个多线程的例子来模似。如果有并发请求的测试工具可能效果会更好。

private void testLock(){
  ExecutorService executorService= Executors.newFixedThreadPool(5);

  executorService.submit(new Runnable() {
    @Override
    public void run() {
      productAppService.invoke2(0);
    }
  });
  executorService.submit(new Runnable() {
    @Override
    public void run() {
      productAppService.invoke2(0);
    }
  });
  executorService.submit(new Runnable() {
    @Override
    public void run() {
      productAppService.invoke2(0);
    }
  });
  executorService.submit(new Runnable() {
    @Override
    public void run() {
      productAppService.invoke2(1);
    }
  });
  executorService.submit(new Runnable() {
    @Override
    public void run() {
      productAppService.invoke2(2);
    }
  });
  executorService.shutdown();
}

测试结果如下,0,1,2三个请求未被阻塞,后面的两个0被阻塞。

分布式版本

分布式的除了锁机制不同之外其它的测试方法相同,这里只贴出锁的部分:

public void invoke2(Integer productId) {
  RLock lock=this.redissonService.getRedisson().getLock(productId.toString());
  try {
    boolean locked=lock.tryLock(3000,500, TimeUnit.MILLISECONDS);
    if(locked){
      Thread.sleep(1000);
      System.out.print("productId:" + productId+" time:"+new Date());
    }
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  finally {
    lock.unlock();
  }

}

嵌入式的缺点

比较明显的就是锁的逻辑与业务逻辑混合在一起,增加了程序复杂度而且也不利于锁机制的更替。

注解式

能否将锁的逻辑隐藏起来,通过在特定方法上增加注解来实现呢?就像Spring Cache的应用。当然是可以的,这里我们只需要解决如下三个问题:

定义注解

锁一般有如下几个属性:

  • key,锁对象的标识,就是上面提到的方法的某些参数。一般由方法所属类的完全限定名,方法名以及指定的参数构成。
  • maximumWaiteTime,最大等待时间,避免线程死循环。
  • expirationTime,锁的生命周期,可以有效避免因特殊原因未释放锁导致其它线程永远获取不到锁的局面。
  • timeUnit,配合上面两个属性使用,时间单位。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestLockable {

  String[] key() default "";

  long maximumWaiteTime() default 2000;

  long expirationTime() default 1000;

  TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

实现注解

由于我们的目标是注解式锁,这里通过AOP的方式来实现,具体依赖AspectJ,创建一个拦截器:

public abstract class AbstractRequestLockInterceptor {

  protected abstract Lock getLock(String key);

  protected abstract boolean tryLock(long waitTime, long leaseTime, TimeUnit unit,Lock lock) throws InterruptedException;

  /**
   * 包的表达式目前还有待优化 TODO
   */
  @Pointcut("execution(* com.chanjet.csp..*(..)) && @annotation(com.chanjet.csp.product.core.annotation.RequestLockable)")
  public void pointcut(){}

  @Around("pointcut()")
  public Object doAround(ProceedingJoinPoint point) throws Throwable{
    Signature signature = point.getSignature();
    MethodSignature methodSignature = (MethodSignature) signature;
    Method method = methodSignature.getMethod();
    String targetName = point.getTarget().getClass().getName();
    String methodName = point.getSignature().getName();
    Object[] arguments = point.getArgs();

    if (method != null && method.isAnnotationPresent(RequestLockable.class)) {
      RequestLockable requestLockable = method.getAnnotation(RequestLockable.class);

      String requestLockKey = getLockKey(method,targetName, methodName, requestLockable.key(), arguments);
      Lock lock=this.getLock(requestLockKey);
      boolean isLock = this.tryLock(requestLockable.maximumWaiteTime(),requestLockable.expirationTime(), requestLockable.timeUnit(),lock);
      if(isLock) {
        try {
          return point.proceed();
        } finally {
          lock.unlock();
        }
      } else {
        throw new RuntimeException("获取锁资源失败");
      }
    }

    return point.proceed();
  }

  private String getLockKey(Method method,String targetName, String methodName, String[] keys, Object[] arguments) {

    StringBuilder sb = new StringBuilder();
    sb.append("lock.").append(targetName).append(".").append(methodName);

    if(keys != null) {
      String keyStr = Joiner.on(".").skipNulls().join(keys);
      if(!StringUtils.isBlank(keyStr)) {
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] parameters =discoverer.getParameterNames(method);
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(keyStr);
        EvaluationContext context = new StandardEvaluationContext();
        int length = parameters.length;
        if (length > 0) {
          for (int i = 0; i < length; i++) {
            context.setVariable(parameters[i], arguments[i]);
          }
        }
        String keysValue = expression.getValue(context, String.class);
        sb.append("#").append(keysValue);
      }
    }
    return sb.toString();
  }

}

注意如下几点:

为什么会存在抽象方法?那是为下面的将注解机制与具体的锁实现解耦服务的,目的是希望注解式锁能够得到复用也便于扩展。锁的key生成规则是什么?前缀一般是方法所在类的完全限定名,方法名称以及spel表达式来构成,避免重复。SPEL表达式如何支持?

LocalVariableTableParameterNameDiscoverer它在Spring MVC解析Controller的参数时有用到,可以从一个Method对象中获取参数名称列表。
SpelExpressionParser是标准的spel解析器,利用上面得来的参数名称列表以及参数值列表来获取真实表达式。

问题

基于aspectj的拦截器,@Pointcut中的参数目前未找到动态配置的方法,如果有解决方案的可以告诉我。

将注解机制与具体的锁实现解耦

注解式锁理论上应该与具体的锁实现细节分离,客户端可以任意指定锁,可以是单机下的ReentrantLock也可以是基于redis的分布式锁,当然也可以是基于zookeeper的锁,基于此目的上面我们创建的AbstractRequestLockInterceptor这个拦截器是个抽象类。看下基于redis的分布式锁的子类实现:

@Aspect
public class RedisRequestLockInterceptor extends AbstractRequestLockInterceptor {

  @Autowired
  private RedissonService redissonService;

  private RedissonClient getRedissonClient(){
    return this.redissonService.getRedisson();
  }

  @Override
  protected Lock getLock(String key) {
    return this.getRedissonClient().getLock(key);
  }

  @Override
  protected boolean tryLock(long waitTime, long leaseTime, TimeUnit unit,Lock lock) throws InterruptedException {
    return ((RLock)lock).tryLock(waitTime,leaseTime,unit);
  }
}

注解式锁的应用

只需要在需要同步的方法上增加@RequestLockable,然后根据需要指定或者不指定key,也可以根据实际场景配置锁等待时间以及锁的生命周期。

  @RequestLockable(key = {"#productId"})
  public void invoke3(Integer productId) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.print("productId:" + productId+" time:"+new Date());
  }

当然为了拦截器生效,我们需要在配置文件中配置上拦截器。

<bean class="com.product.api.interceptor.RedisRequestLockInterceptor"></bean>
<aop:aspectj-autoproxy proxy-target-class="true"/>

注解式锁的优点:锁的逻辑与业务代码完全分离,降低了复杂度。灵活的spel表达式可以灵活的构建锁的key。支持多种锁,可以随意切换而不影响业务代码。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java注解的Retention和RetentionPolicy实例分析

    本文实例讲述了Java注解的Retention和RetentionPolicy.分享给大家供大家参考,具体如下: 一 源码赏析 1 源码 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { RetentionPolicy value(); } public enum RetentionPolicy { //此注解类型的信

  • Java基于反射机制实现全部注解获取的方法示例

    本文实例讲述了Java基于反射机制实现全部注解获取的方法.分享给大家供大家参考,具体如下: 一 代码 class Info{ //给mytoString方法加了2个内建Annotation @Deprecated @SuppressWarnings(value = "This is a waring!") public String mytoString(){ return "hello world"; } } class GetAnnotations{ publi

  • java元注解@Inherited的使用详解

    1.先看源码文档 /** * Indicates that an annotation type is automatically inherited. If * an Inherited meta-annotation is present on an annotation type * declaration, and the user queries the annotation type on a class * declaration, and the class declaratio

  • Java使用@Validated注解进行参数验证的方法

    目前项目中大部分代码进行参数验证都是写代码进行验证,为了提升方便性和代码的简洁性,所以整理了下使用注解进行参数验证.使用效果如下: // 要验证的实体类 @Data public class User implements Serializable { @NotBlank(message = "id不能为空!",groups = Update.class) protected String id = ""; @NotBlank(message = "商户i

  • Java基础之自动装箱,注解操作示例

    本文实例讲述了Java基础之自动装箱,注解操作.分享给大家供大家参考,具体如下: 示例代码: 手动装箱,手动拆箱 Integer iOb=new Integer(100);//手动装箱 int i=iOb.intValue(); //手动拆箱 System.out.println(i+" "+iOb); 自动装箱,自动拆箱 Integer iOb=1000; int i=iOb; System.out.println(i+" "+iOb); 静态导入:可以直接通过静

  • Java自定义注解用法实例小结

    本文实例讲述了Java自定义注解用法.分享给大家供大家参考,具体如下: 一 自定义注解语法 [public] @interface Annotation的名称 { [数据类型 变量名称();] } 要自定义注解,需要使用@interface方式进行定义,在定义注解时也可以定义各种变量,但是变量之后必须使用括号(). 提示:使用@interface就相对于继承了Annotation接口.在程序中使用@interface声明Annotation,那么此Annotation实际相对于继承了Annota

  • Java注解如何基于Redission实现分布式锁

    这篇文章主要介绍了Java注解如何基于Redission实现分布式锁,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.定义注解类 @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DistributedLock { //锁名称 String lockName() default ""; /

  • Java基于注解实现的锁实例解析

    背景 某些场景下,有可能一个方法不能被并发执行,有可能一个方法的特定参数不能被并发执行.比如不能将一个消息发送多次,创建缓存最好只创建一次等等.为了实现上面的目标我们就需要采用同步机制来完成,但同步的逻辑如何实现呢,是否会影响到原有逻辑呢? 嵌入式 这里讲的嵌入式是说获取锁以及释放锁的逻辑与业务代码耦合在一起,又分分布式与单机两种不同场景的不同实现. 单机版本 下面方法,每个productId不允许并发访问,所以这里可以直接用synchronized来锁定不同的参数. @Service publ

  • Java实现基于TCP的通讯程序实例解析

    Java中的TCP通信程序 TCP可以实现两台计算机之间的数据交互通信的两端,要严格区分客户端与服务端 两端通信时的步骤: 1.服务端程序,需要事先启动,等待客户端连接 2.客户端主动连接服务器端,才能成功通信,服务器端不可以主动链接客户端 在java中两个类用于实现TCP通信程序: 客户端: java.net.Socket 类表示.创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建 立连接开始通信. 服务端: java.net.ServerSocket 类表示.创建 Ser

  • Java多线程并发编程和锁原理解析

    这篇文章主要介绍了Java多线程并发编程和锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等方案后,问题得到解决. 加锁方案见下文. 二.乐观锁 & 悲观锁 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁

  • Java原子变量类原理及实例解析

    这篇文章主要介绍了Java原子变量类原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.原子变量类简介 为何需要原子变量类 保证线程安全是 Java 并发编程必须要解决的重要问题.Java 从原子性.可见性.有序性这三大特性入手,确保多线程的数据一致性. 确保线程安全最常见的做法是利用锁机制(Lock.sychronized)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,那么操作必然是原子性

  • java基于dom4j包实现对XML解析的方法

    本文实例讲述了java基于dom4j包实现对XML解析的方法.分享给大家供大家参考,具体如下: 本例中的xml文件内容如下: <?xml version = "1.0" encoding="UTF-8"?> <!-- Copyright 难免有错 这是注释--> <自定义的> <!-- iloveyou --> <你喜欢的名字就好> <who a = "i"></who

  • Java基于正则表达式实现xml文件的解析功能详解

    本文实例讲述了Java基于正则表达式实现xml文件的解析功能.分享给大家供大家参考,具体如下: 这是我通过正则表达式实现的xml文件解析工具,有些XHTML文件中包含特殊符号,暂时还无法正常使用. 设计思路:常见的xml文件都是单根树结构,工具的目的是通过递归的方式将整个文档树装载进一个Node对象.xml文档树上的每一个节点都能看做一个Node对象,它拥有title.attribute和text三个自身变量以及一个childrenNode集合用来存放子节点,使用正则表达式完整装载. 一.编写N

  • java阻塞队列实现原理及实例解析

    这篇文章主要介绍了java阻塞队列实现原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 阻塞队列与普通队列的不同在于.当队列是空的时候,从队列中获取元素的操作将会被阻塞,或者当队列满时,往队列里面添加元素将会被阻塞.试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素.同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完

  • Java内存模型原子性原理及实例解析

    这篇文章主要介绍了Java内存模型原子性原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文就具体来讲讲JMM是如何保证共享变量访问的原子性的. 原子性问题 原子性是指:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行. 下面就是一段会出现原子性问题的代码: public class AtomicProblem { private static Logger logger = LoggerFactory.

  • Java反射技术详解及实例解析

    前言 相信很多人都知道反射可以说是Java中最强大的技术了,它可以做的事情太多太多,很多优秀的开源框架都是通过反射完成的,比如最初的很多注解框架,后来因为java反射影响性能,所以被运行时注解APT替代了,java反射有个开源框架jOOR相信很多人都用过,不过我们还是要学习反射的基础语法,这样才能自己写出优秀的框架,当然这里所讲的反射技术,是学习Android插件化技术.Hook技术等必不可少的! 一.基本反射技术   1.1 根据一个字符串得到一个类 getClass方法 String nam

  • Java基于redis实现分布式锁

    为了保证一个在高并发存场景下只能被同一个线程操作,java并发处理提供ReentrantLock或Synchronized进行互斥控制.但是这仅仅对单机环境有效.我们实现分布式锁大概通过三种方式. redis实现分布式锁 数据库实现分布式锁 zk实现分布式锁 实际上这三种和java对比看属于一类.都是属于程序外部锁. 原理剖析 上述三种分布式锁都是通过各自为依据对各个请求进行上锁,解锁从而控制放行还是拒绝.redis锁是基于其提供的setnx命令. setnx当且仅当key不存在.若给定key已

随机推荐