• 使用自定义注解实现Redis分布式锁


      😊 @ 作者: 一恍过去
      🎊 @ 社区: Java技术栈交流
      🎉 @ 主题: 使用自定义注解实现Redis分布式锁
      ⏱️ @ 创作时间: 2022年06月29日

      1、RedisLockServer

      定义Redis上锁、解锁实现类

      import lombok.extern.slf4j.Slf4j;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.data.redis.core.script.RedisScript;
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      import java.util.Collections;
      import java.util.List;
      import java.util.concurrent.TimeUnit;
      
      
      @Component
      @Slf4j
      public class RedisLockServer {
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
      
          public Boolean setLock(String lockKey, String value, long time) {
              if(time<=0){
                  // 不设置过期时间
                  return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
              }
              return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, time, TimeUnit.MILLISECONDS);
          }
      
          public void deleteLock(String lockKey, String value) {
              List<String> lockKeys = Collections.singletonList(lockKey);
              String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return 1 else return 0 end";
              RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
              // 删除锁
              stringRedisTemplate.execute(luaScript, lockKeys, value);
          }
      }
      
      • 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

      2、RedisLock

      定义“自定义注解”,用于切面实现分布式锁

      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface RedisLock {
          /**
           * 上锁的方法中,key所在参数的位置,索引从0开始
           * @return
           */
          int keyNum();
      
          /**
           * 上锁时长,默认设置时间
           * @return
           */
          long lockTime() default 0;
      
          /**
           * 尝试时间,设置时间内通过自旋一致尝试获取锁,默认0ms
           * @return
           */
          long tryTime() default 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

      字段说明:

      • keyNum:
        keyNum表示在被@RedisLock 修饰的方法上,第几个参数表示上锁的key,默认第一个参数,也可以指定key的位置,并且参数的名称必须要为keyName
      // key为第一个参数值
      void test(String keyName){}
      
      // key为第三个参数值
      @RedisLock(keyNum=2)
      void test(int a,int b,String keyName){}
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • lockTime:
        lockTime表示上锁过期时间,默认为0,表示不进行上锁处理;
          public Boolean setLock(String lockKey, String value, long time) {
              if(time<=0){
                  // 不设置过期时间
                  return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
              }
              return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, time, TimeUnit.MILLISECONDS);
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • tryTime:
        tryTime表示尝试获取锁的时间,当设置的时间内,通过自旋的方式一直获取锁

      3、RedisLockAspect

      切面类定义,代码如下:

      import lhz.lx.config.RedisLockServer;
      import lombok.extern.slf4j.Slf4j;
      import org.aspectj.lang.JoinPoint;
      import org.aspectj.lang.ProceedingJoinPoint;
      import org.aspectj.lang.Signature;
      import org.aspectj.lang.annotation.*;
      import org.aspectj.lang.reflect.CodeSignature;
      import org.aspectj.lang.reflect.MethodSignature;
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      import java.lang.reflect.Method;
      import java.util.UUID;
      
      @Aspect
      @Component
      @Slf4j
      public class RedisLockAspect {
      
          @Resource
          private RedisLockServer redisLockServer;
      
          private static final ThreadLocal<String> VALUE_THREAD = new ThreadLocal<>();
      
          private static final ThreadLocal<String> KEY_THREAD = new ThreadLocal<>();
      
          private static final ThreadLocal<Boolean> LOCK_THREAD = new ThreadLocal<>();
      
          @Pointcut("@annotation(lhz.lx.aspect.RedisLock)")
          public void lockPoint() {
      
          }
      
          /**
           * 环绕通知,调用目标方法
           */
          @Around("lockPoint()")
          public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
              // 记录方法执行开始时间
              long startTime = System.currentTimeMillis();
      
              Object[] args = proceedingJoinPoint.getArgs();
              if(args.length<=0){
                  throw new RuntimeException("keyName不存在!");
              }
              String[] argNames = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();
      
              Signature signature = proceedingJoinPoint.getSignature();
              MethodSignature methodSignature = (MethodSignature) signature;
              Method method = methodSignature.getMethod();
              RedisLock lock = method.getAnnotation(RedisLock.class);
      
              int keyNum = lock.keyNum();
              if(!"keyName".equals(argNames[keyNum])){
                  throw new RuntimeException("keyName不存在!");
              }
              String key = args[keyNum].toString();
              long lockTime = lock.lockTime();
              long tryTime = lock.tryTime();
              String value = UUID.randomUUID().toString();
      
              VALUE_THREAD.set(value);
              KEY_THREAD.set(key);
      
              log.info("分布式锁上锁,key:{},value:{},lockTime:{}",key,value,lockTime);
      
              Boolean setLock = redisLockServer.setLock(key, value, lockTime);
              while (!setLock){
                  // 重试
                  setLock = redisLockServer.setLock(key, value, lockTime);
                  if(System.currentTimeMillis()-startTime>tryTime) {
                      LOCK_THREAD.set(false);
                      log.error("上锁失败");
                      throw new RuntimeException("上锁失败!");
                  }
              }
              log.info("分布式锁上锁成功,key:{},value:{},lockTime:{}",key,value,lockTime);
              LOCK_THREAD.set(true);
              // 调用目标方法
              return proceedingJoinPoint.proceed();
          }
      
          /**
           * 处理完请求后执行
           *
           * @param joinPoint 切点
           */
          @AfterReturning(value = "lockPoint()", returning = "jsonResult")
          public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
              handleData();
          }
      
          /**
           * 拦截异常操作
           *
           * @param joinPoint 切点
           * @param e         异常
           */
          @AfterThrowing(value = "lockPoint()", throwing = "e")
          public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
              handleData();
          }
      
          private void handleData() {
              try {
                  String value = VALUE_THREAD.get();
                  String key = KEY_THREAD.get();
                  if(LOCK_THREAD.get()) {
                      log.info("分布式锁解锁,key:{},value:{}", key, value);
                      redisLockServer.deleteLock(key, value);
                  }
              } catch (Exception exception) {
                  exception.printStackTrace();
              } finally {
                  VALUE_THREAD.remove();
                  KEY_THREAD.remove();
                  LOCK_THREAD.remove();
              }
          }
      }
      
      • 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
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120

      4、开启自动代理

      因为说明AOP无法拦截类内部的方法之间的调用,需要对启动类加上@EnableAspectJAutoProxy配置,代码如下:

      @SpringBootApplication
      @EnableAspectJAutoProxy(exposeProxy = true)
      public class RedisDemoApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(RedisDemoApplication.class, args);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      比如方法直接的调用,A方法调用B方法,使用方式如下:

      public class RedisServiceImpl implements RedisService{
      	public void A() {
      		// 方法类调用
      		RedisServiceImpl service = (RedisServiceImpl) AopContext.currentProxy();
      		 service.B();
      	}
      	
      	@RedisLock
      	public void B() {
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      5、使用

      注意: 在调用使用分布式锁的方法时,需要进try…catch…,并且在catch中处理上锁失败的情况;

      TestController:

      @RestController
      @RequestMapping
      @Slf4j
      public class TestController {
      
          @Resource
          private RedisService redisService;
      
          @GetMapping(value = "/test")
          public String test() {
              redisService.test();
              return "success";
          }
      
          @GetMapping(value = "/test2")
          public String test2() {
              String key = UUID.randomUUID().toString();
              redisService.test2(key);
              return "success";
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

      RedisService :

      public interface RedisService {
          /**
           *  方法内部调用使用锁
           */
          void test();
      
          /**
           * 方法直接调用使用锁
           * @param keyName
           */
          void test2(String keyName);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      RedisServiceImpl:

      @Service
      @Slf4j
      public class RedisServiceImpl implements RedisService{
      
          /**
           *  方法内部调用使用锁
           */
          @Override
          public void test() {
              log.info("方法内部调用使用锁");
      
              // 调用内部方法
              RedisServiceImpl service = (RedisServiceImpl) AopContext.currentProxy();
              // 在调用使用分布式锁的方法时,需要进try...catch...,并且在catch中处理上锁失败的情况
              try {
                  service.testLock("test11");
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      
          /**
           * 方法直接调用使用锁
           */
          @Override
          @RedisLock(keyNum = 0)
          public void test2(String keyName) {
              log.info("方法直接调用使用锁");
          }
          /**
           * 被上锁的方法中,一定要存在一个叫"keyName"的参数
           * @param keyName
           */
          @RedisLock
          public void testLock(String keyName) throws InterruptedException {
              log.info(keyName);
          }
      }
      
      • 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

      6、测试

      测试一:
      正常上锁情况
      在这里插入图片描述

      测试二:
      设置tryTime,并且tryTime小于lockTime;当这样配置时,在第一个线程没有结束时,第二个线程,超过tryTime就会出现上锁失败;

      修改代码如下:

          @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 2000)
          public void testLock(String keyName) throws InterruptedException {
              log.info(keyName);
               TimeUnit.SECONDS.sleep(5);
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5

      快速请求两次接口,截图如下:
      在这里插入图片描述
      通过截图可以看到,在第一个线程上锁后,过了2000ms出现了上锁失败的提示;

      测试三:
      设置tryTime,并且tryTime大于lockTime;当这样配置时,不会出现上锁失败,并且第二个线程会一直等到第一个线程结束;
      修改代码如下:

          @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 4000)
          public void testLock(String keyName) throws InterruptedException {
              log.info(keyName);
               TimeUnit.SECONDS.sleep(5);
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5

      快速请求两次接口,截图如下:
      在这里插入图片描述

      通过截图可以看到,在第一个线程上锁后,超过了3000ms后,第二个线程开始上锁成功

    • 相关阅读:
      .NET 7.0 重磅发布及资源汇总
      C语言实现单链表
      Django实战项目-学习任务系统-用户注册
      【英语:语法基础】C3.日常对话-购物专题
      Xilinx FPGA 7系列 GTX/GTH Transceivers (2)--IBERT
      低代码平台全解析:衍生历程、优势呈现与未来趋势一览无余
      公务员备考(二十) 申论
      在java的继承中你是否有这样的疑惑?
      liunx:进程概念
      Spark 连接 Mongodb 批量读取数据
    • 原文地址:https://blog.csdn.net/zhuocailing3390/article/details/125474626