切断面と注解によるRedis分布式ロックを実現しました。

35776 ワード

説明
  • SprigEL表現に基づいて、動的構成
  • 切断面に基づいて、シームレスに切断する
  • は、鍵の取得に失敗したときの挙動をサポートしています。異常を投げても待ち続けてもいいです。2つの方法のロックは、再試行を待っています。1つは直接
  • から退出します。
    ソースの住所:https://github.com/shawntime/shawn-common-utils/tree/master/src/main/java/com/shawntime/common/lock
    使い方
    @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]
    "
    + "then
    "
    + " return redis.call(\"del\",KEYS[1])
    "
    + "else
    "
    + " return 0
    "
    + "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分散ロックの3つの実装
    一つ目は、setnx()、get()、getsset()を使用する方法です。
    > 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の値current ExpireTimeに戻ります。
  • は、current ExpireTimeとoldExpireTimeが等しいかどうかを判断し、等しい場合、現在のgetsset設定が成功したと説明し、ロックを取得した。もし同じではないならば、このロックはまた他の要求によって取得されたと説明しています。現在の要求は直接に失敗に戻ります。または再試行を続けます。
  • ロックを取得した後、現在のスレッドは自分の業務処理を開始することができ、処理が完了した後、自分の処理時間とロックに対するタイムアウト時間を比較し、ロック設定のタイムアウト時間より小さい場合は、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 miliseconds][NX|XX]により実現しました。
    EX second:設定キーの賞味期限はsecond秒です。SET key value EX second効果はSETEX key second valueに相当します。PX milisecond:設定キーの賞味期限はmilisecond msです。SET key value PX milisecond効果はPSETEX key milisecond 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を使ってはいけません。他の人のロックを誤魔化す可能性があります。例えば、この錠は10 sかかりましたが、10 sより処理時間が長くなりました。この錠は自動的に期限が切れました。他の人に取り去られました。また鍵をかけなおしました。この時、Redisを呼び出します。delは他の人が作ったロックを削除します。luaスクリプトを使って、まずgetして、delを行います。
    private static final String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]
    " + "then
    " + " return redis.call(\"del\",KEYS[1])
    " + "else
    " + " return 0
    " + "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 keys, List 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);