• 注册
  • 后端开发博客 后端开发博客 关注:0 内容:2417

    基于切面和注解的Redis分布式锁实现

  • 查看作者
  • 打赏作者
  • 当前位置: 职业司 > 后端开发 > 后端开发博客 > 正文
    • 后端开发博客
    • 描述

      • 基于SpringEL表达式,动态配置
      • 基于切面,无缝切入
      • 支持获取锁失败时的行为,抛出异常还是继续等待,两种方式的锁,一种等待重试,一种直接退出

      源码地址:github.com/shawntime/s…

      使用方法

      @RedisLockable(key = {"#in.activityId", "#in.userMobile"}, expiration = 120, isWaiting = true, retryCount = 2)
      @Override
      public PlaceOrderOut placeOrder(OrderIn in) {
      // ------
      }
      复制代码

      代码实现

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.METHOD)
      public @interface RedisLockable {
      String prefix() default "";
      String[] key() default "";
      long expiration() default 60;
      boolean isWaiting() default false; //锁是否等待,默认为不等待
      int retryCount() default -1; // 锁等待重试次数,-1未不限制
      int retryWaitingTime() default 10; // 锁等待重试间隔时间,默认10毫秒
      }
      复制代码
      @Aspect
      @Component
      public class RedisLockInterceptor {
      private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
      private static final ExpressionParser PARSER = new SpelExpressionParser();
      @Pointcut("@annotation(com.shawntime.common.lock.RedisLockable)")
      public void pointcut() {
      }
      @Around("pointcut()")
      public Object doAround(ProceedingJoinPoint point) throws Throwable {
      MethodSignature methodSignature = (MethodSignature) point.getSignature();
      Method targetMethod = AopUtils.getMostSpecificMethod(methodSignature.getMethod(), point.getTarget().getClass());
      String targetName = point.getTarget().getClass().getName();
      String methodName = point.getSignature().getName();
      Object[] arguments = point.getArgs();
      RedisLockable redisLock = targetMethod.getAnnotation(RedisLockable.class);
      long expire = redisLock.expiration();
      String redisKey = getLockKey(redisLock, targetMethod, targetName, methodName, arguments);
      String uuid;
      if (redisLock.isWaiting()) {
      uuid = waitingLock(redisKey, expire, redisLock.retryCount(), redisLock.retryWaitingTime());
      } else {
      uuid = noWaitingLock(redisKey, expire);
      }
      if (StringUtils.isNotEmpty(uuid)) {
      try {
      return point.proceed();
      } finally {
      RedisLockUtil.unLock(redisKey, uuid);
      }
      } else {
      throw new RedisLockException(redisKey);
      }
      }
      private String getLockKey(RedisLockable redisLock, Method targetMethod,
      String targetName, String methodName, Object[] arguments) {
      String[] keys = redisLock.key();
      String prefix = redisLock.prefix();
      StringBuilder sb = new StringBuilder("lock.");
      if (StringUtils.isEmpty(prefix)) {
      sb.append(targetName).append(".").append(methodName);
      } else {
      sb.append(prefix);
      }
      if (keys != null) {
      String keyStr = Joiner.on("+ '.' +").skipNulls().join(keys);
      EvaluationContext context = new StandardEvaluationContext(targetMethod);
      String[] parameterNames = DISCOVERER.getParameterNames(targetMethod);
      for (int i = 0; i < parameterNames.length; i++) {
      context.setVariable(parameterNames[i], arguments[i]);
      }
      Object key = PARSER.parseExpression(keyStr).getValue(context);
      sb.append("#").append(key);
      }
      return sb.toString();
      }
      private String noWaitingLock(String key, long expire) {
      return RedisLockUtil.lock(key, expire);
      }
      private String waitingLock(String key, long expire, int retryCount, int retryWaitingTime)
      throws InterruptedException {
      int count = 0;
      while (retryCount == -1 || count <= retryCount) {
      String uuid = noWaitingLock(key, expire);
      if (!StringUtils.isEmpty(uuid)) {
      return uuid;
      }
      try {
      TimeUnit.MILLISECONDS.sleep(retryWaitingTime);
      } catch (InterruptedException e) {
      throw e;
      }
      count++;
      }
      return null;
      }
      }
      复制代码
      /**
      * 分布式锁工具类
      */
      public final class RedisLockUtil {
      private static final int DEFAULT_EXPIRE = 60;
      private static final String SCRIPT =
      "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
      + "then\n"
      + "    return redis.call(\"del\",KEYS[1])\n"
      + "else\n"
      + "    return 0\n"
      + "end";
      private RedisLockUtil() {
      super();
      }
      /**
      *
      * @param key 锁的key
      * @return 返回value为null,则锁失败,不为null则锁成功
      */
      public static String lock(String key) {
      return lock(key, DEFAULT_EXPIRE);
      }
      public static boolean lock(String key, String value) {
      return lock(key, value, DEFAULT_EXPIRE);
      }
      public static String lock(String key, long expire) {
      String value = UUID.randomUUID().toString();
      boolean nx = SpringRedisUtils.setNX(key, value, expire);
      return nx ? value : null;
      }
      public static boolean lock(String key, String value, long expire) {
      return SpringRedisUtils.setNX(key, value, expire);
      }
      public static void unLock(String key, String value) {
      SpringRedisUtils.lua(SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
      }
      }
      复制代码

      redis分布式锁的三种实现

      第一种:使用setnx()、get()、getset()方法

      > SETNX命令(SET if Not eXists)\
      语法:SETNX key value\
      功能:原子性操作,当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。\
      GETSET命令\
      语法:GETSET key value\
      功能:将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。\
      GET命令\
      语法:GET key\
      功能:返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。\
      DEL命令\
      语法:DEL key [KEY …]\
      功能:删除给定的一个或多个 key ,不存在的 key 会被忽略。
      复制代码
      • setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
      • get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
      • 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
      • 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
      • 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
      /**
      * 等待锁
      *
      * @param key    redis key
      * @param expire 过期时间,单位秒
      * @return true:加锁成功,false,加锁失败
      */
      private boolean waitingLock(String key, long expire, int retryCount) {
      int count = 0;
      while (retryCount == -1 || count <= retryCount) {
      if (noWaitingLock(key, expire)) {
      return true;
      }
      try {
      TimeUnit.MILLISECONDS.sleep(10);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      count++;
      }
      return false;
      }
      /**
      * 加锁
      *
      * @param key    redis key
      * @param expire 过期时间,单位秒
      * @return true:加锁成功,false,加锁失败
      */
      private boolean noWaitingLock(String key, long expire) {
      long value = System.currentTimeMillis() + expire * 1000;
      long status = redisClient.setnx(key, value);
      if (status == 1) {
      return true;
      }
      long oldExpireTime = Long.parseLong(redisClient.get(key, "0", false));
      if (oldExpireTime < System.currentTimeMillis()) {
      long newExpireTime = System.currentTimeMillis() + expire * 1000;
      String currentExpireTimeStr = redisClient.getSet(key, String.valueOf(newExpireTime));
      if (StringUtils.isEmpty(currentExpireTimeStr)) {
      return true;
      }
      long currentExpireTime = Long.parseLong(currentExpireTimeStr);
      if (currentExpireTime == oldExpireTime) {
      return true;
      }
      }
      return false;
      }
      private void unLock(String key, long startTime, long expire) {
      long parseTime = System.currentTimeMillis() - startTime;
      if (parseTime <= expire * 1000) {
      redisClient.del(key);
      }
      }
      复制代码

      通过SET key value [EX seconds] [PX milliseconds] [NX|XX]实现

      EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
      PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
      NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
      XX :只在键已经存在时,才对键进行设置操作。

      private boolean noWaitingLock2(String key, String uuid, long expire) {
      String value = redisClient.setnx(key, uuid, expire);
      return value != null;
      }
      private boolean waitingLock2(String key, String uuid, long expire, int retryCount) {
      int count = 0;
      while (retryCount == -1 || count <= retryCount) {
      if (noWaitingLock2(key, uuid, expire)) {
      return true;
      }
      try {
      TimeUnit.MILLISECONDS.sleep(10);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      count++;
      }
      return false;
      }
      复制代码

      删除锁的机制直接使用del不可行,因为有可能导致误删别人的锁的情况。

      比如,这个锁我上了10s,但是我处理的时间比10s更长,到了10s,这个锁自动过期了,被别人取走了,并且对它重新上锁了。那么这个时候,我再调用Redis::del就是删除别人建立的锁了。
      使用lua脚本,先进行get,再进行del

      private static final String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
      "then\n" +
      "    return redis.call(\"del\",KEYS[1])\n" +
      "else\n" +
      "    return 0\n" +
      "end";
      private void unLock2(String key, String uuid) {
      Object result = redisClient.lua(script, Collections.singletonList(key), Collections.singletonList(uuid));
      System.out.println(result);
      }
      public Object lua(final String script, List<String> keys, List<String> args) {
      Jedis jedis = null;
      try {
      jedis = pool.getResource();
      return jedis.eval(script, keys, args);
      } catch (Exception ex) {
      LOGGER.error(ex);
      return 0;
      } finally {
      returnResource(jedis);
      }
      }
      复制代码

      Redissons实现分布式锁

      RLock rLock = redisson.getLock(lockKey);
      long expired = lock.expire();
      boolean isLock = rLock.tryLock(expired, TimeUnit.SECONDS);
      if (isLock) {
      try {
      // 处理
      } finally {
      rLock.unlock();
      }
      }
      rLock.tryLock(3, expired, TimeUnit.SECONDS);
      复制代码

      请登录之后再进行评论

      登录

      手机阅读天地(APP)

      • 微信公众号
      • 微信小程序
      • 安卓APP
      手机浏览,惊喜多多
      匿名树洞,说我想说!
      问答悬赏,VIP可见!
      密码可见,回复可见!
      即时聊天、群聊互动!
      宠物孵化,赠送礼物!
      动态像框,专属头衔!
      挑战/抽奖,金币送不停!
      赶紧体会下,不会让你失望!
    • 实时动态
    • 签到
    • 做任务
    • 发表内容
    • 偏好设置
    • 到底部
    • 帖子间隔 侧栏位置:
    • 还没有账号?点这里立即注册