AOP+SPel+Redis实现分布式锁的切面

一、背景介绍

随着高并发场景的逐渐增多,各类系统中(尤其是类似秒杀系统这种对于并发度要求很高的场景下)对于分布式锁的需求逐渐增大。

而在分布式锁的代码实现中,往往会存在如下情况

 public void method(){
     // 业务代码
     try{
         boolean b = redisClient.tryLock(key,value);
         if(b){
             // ......逻辑判断
         }
         // 获取锁后所要执行的业务代码
     }catch(Exception e){
         log.error("error");
         throw new Exception();
     }finally{
         if(redisClient.get(key).equals(value)){
             redisClient.tryRelease(key);
         }
     }
     // 业务代码
 }

大概写了段比较简单的代码,意思是在很多场景下,都是这样子在实现分布式锁的,具体redisClient是怎么实现的这个根据情况可以自己选择。

这段代码的问题在于,如果需要分布式锁的场景太多,那么整个系统中就会处处都存在这样的try catch finally的代码块,这样会使得整体代码显得十分臃肿,重复代码太多

那么怎么去解决这种使得代码过于臃肿的问题呢?

答案很简单:使用AOP技术,将加入分布式锁的部分以切面的方式剥离出业务代码中

具体怎么实现呢?

二、分布式锁注解+切面的简单实现

首先我们实现一个注解,注解命名建议要直观,让人可以一眼看出该注解的作用

 @Target({ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)
 @Inherited
 @Documented
 public @interface RedisDistributedLock {
     // 锁定的资源的值,即存入redis的key
     String key() default "default_key";
     // 锁保持时间(以毫秒为单位)
     int keepMills() default 1 * 10 * 1000;
 }

关于自定义注解的方式,本文不再赘述,有兴趣的可以自行去了解,

我们希望使用 @RedisDistributedLock这个注解去实现分布式锁,即如下所示。

 @RedisDistributedLock(key = "key_example", keepMills = 2*10*1000)
 public void method(){
     // 业务代码
 }

即我们想在执行method方法之前,可以对 key_example 加锁,如果不设置key,则默认为default_key,释放锁的时间为20秒,如果不加设置,则默认为10秒

那么具体AOP切面的简单实现如下

 @Aspect
 @Component
 @Slf4j
 public class RedisDistributedLockAspect {
     @Autowired
     private RedisClient redisClient;
 ​
     @Around(value = "@annotation(redisDistributedLock)")
     public Object invoke(ProceedingJoinPoint point) throws Throwable {
         Signature signature = point.getSignature();
         MethodSignature methodSignature = (MethodSignature)signature;
         Method targetMethod = methodSignature.getMethod();
         //获取方法上的注解
         RedisDistributedLock redisLock=targetMethod.getAnnotation(RedisDistributedLock.class);
         String key=redisLock.key();
         if(StringUtils.isEmpty(key)){
             key=point.getTarget().getClass().getName()+"."+targetMethod.getName();
         }
         Boolean isLocked = redisClient.tryLock(key, "true", redisLock.keepMills());
         if (!isLocked) {
             logger.debug("get lock failed : " + key);
             return null;
         }
         logger.debug("get lock success : " + key);
         try {
             return proceedingJoinPoint.proceed();
         } catch (Exception e) {
             logger.error("execute locked method occured an exception", e);
         } finally {
             if (redisClient.get(key, String.class).equals(value)) {
                 redisClient.tryRelease(key);
             }
         }
         return null;
     }
 }

三、动态的方法入参作为分布式锁的key

那么通过这种方式就可以实现对于method()方法上锁了,但是这个样还是有点不太方便

如果我想对于 method()方法传入的参数加锁而不是只能写死一个key该怎么办

如下:

 @RedisDistributedLock(key = "param", keepMills = 2*10*1000)
 public void method(String param){
     // 业务代码
 }

如果我们可以动态的对于方法中传入的参数加锁,那么这个注解使用起来就更加的方便了

此处我们可以使用Spel表达式来进行处理,关于SPel表达式,请各位自己去查询资料,我这里只给出了最终的代码

 
 @Aspect
 @Component
 @Slf4j
 public class RedisDistributedLockAspect {
     @Autowired
     private RedisClient redisClient;
 ​
     private static Logger logger = LoggerFactory.getLogger(RedisDistributedLockAspect.class);
 ​
     @Around(value = "@annotation(redisDistributedLock)")
     public Object invoke(ProceedingJoinPoint proceedingJoinPoint, RedisDistributedLock redisDistributedLock) throws Throwable {
         String key = redisDistributedLock.key();
         Object[] arg = proceedingJoinPoint.getArgs();
         Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
         // 获取被拦截方法参数名列表(使用Spring支持类库)
         LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
         String[] parameterNames = localVariableTable.getParameterNames(method);
         // 使用SPEL进行key的解析
         ExpressionParser parser = new SpelExpressionParser();
         // SPEL上下文
         StandardEvaluationContext context = new StandardEvaluationContext();
         // 把方法参数放入SPEL上下文中
         for (int i = 0; i < parameterNames.length; i++) {
             context.setVariable(parameterNames[i], arg[i]);
         }
         // 使用变量方式传入业务动态数据
         if (valueOfKey.matches("^#.*.$")) {
             valueOfKey = parser.parseExpression(key).getValue(context, String.class);
         }
         // 使用UUID生成随机数作为value,避免出现锁被错误释放的问题
         String value = UUID.randomUUID().toString();
         Boolean isLocked = redisClient.tryLock(key, value, redisDistributedLock.keepMills());
         if (!isLocked) {
             logger.debug("get lock failed : " + key);
             return null;
         }
         logger.debug("get lock success : " + key);
         try {
             return proceedingJoinPoint.proceed();
         } catch (Exception e) {
             logger.error("execute locked method occured an exception", e);
         } finally {
             if (redisClient.get(key, String.class).equals(value)) {
                 redisClient.tryRelease(key);
             }
         }
         return null;
     }
 }
 ​

这样子的话,在方法中只要以如下的方式,就可以对于方法中传入的参数加锁了

@RedisDistributedLock(key = "#param", keepMills = 2*10*1000)
public void method(String param){
    // 业务代码
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值