Redis Luaスクリプトによる分散ロック

6815 ワード

1.分散ロック
  • 概念:複数のプロセスが同じシステムにない場合、分散ロックで複数のプロセスのリソースへのアクセスを制御します.
  • シーン:分散型の場合(マルチJVM)、スレッドAとスレッドBは同じJVMではない可能性が高いため、スレッドロックが機能しなくなり、分散型ロックで解決します.

  • 2.redis Templateは分布式ロックを実現する
    2.1 DistributedLockインタフェース
    public interface DistributedLock {
        /**
         *    
         * @param requestId   id uuid
         * @param key  key
         * @param expiresTime      ms
         * @return       true
         */
        boolean lock(String requestId,String key,int expiresTime);
    
        /**
         *    
         * @param key  key
         * @param requestId   id uuid
         * @return      true
         */
        boolean releaseLock(String key,String requestId);
    }
    

    2.2 Spring Bootでのredis Templateの構成
    参照redis template構成
    2.3ロック実装の取得
    ロックを取得するにはluaスクリプトを使用します.これにより、ロックと失効時間の原子性を保証できます.ロックの取得に成功した後、異常に終了し、ロックが解放されないという問題を回避します.
  • luaスクリプトluaスクリプトはアプリケーションに配置する.propertiesではjedisのsetnxコマンドで直接失効時間を設定できますが、Spring Boot redis Templateを使用して失効時間のあるapiは見つかりません.
  • lua.lockScript=if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then  return redis.call('expire',KEYS[1],ARGV[2])  else return 0 end
    
  • ロック実装
  • public boolean lock(String requestId, String key, int expiresTime) {
          DefaultRedisScript longDefaultRedisScript = new DefaultRedisScript<>(luaScript.lockScript, Long.class);
            Long result = stringRedisTemplate.execute(longDefaultRedisScript, Collections.singletonList(key), requestId,String.valueOf(expiresTime));
          return result == 1;
        }
    

    2.4ロック解除実装
    ロックの解放は、解放するロックが自分で取得したロックであることを保証する.他の人が手に入れた鍵を解放すると、めちゃくちゃになります.
  • ロックluaスクリプト
  • を解放
    lua.releaseLockScript=if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
    
  • ロック解除
  • を実現する.
    @Override
        public boolean releaseLock(String key, String requestId) {
            DefaultRedisScript longDefaultRedisScript = new DefaultRedisScript<>(luaScript.releaseLockScript, Long.class);
            Long result = stringRedisTemplate.execute(longDefaultRedisScript, Collections.singletonList(key), requestId);
            return result == 1;
        }
    

    3分散ロックの使用
    プロジェクトで分散ロックを使用するとSpring AOPと組み合わせて使用できます.
    3.1ロック注記
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface AddLock {
        //spel   
        String spel() ;
        //        
        int expireTime() default 10;
        //log  
        String logInfo() default "";
    }
    

    3.2 AOP切麺
    ロックを取得するキーはSpel式を解析することによって行われ,SpelUtilのコードはSpelUtilを参照する.
    @Aspect
    @Component
    public class AddLockAspect {
    
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        @Resource
        private DistributedLock distributedLock;
    
    
        @Pointcut("@annotation(com.example.demo.annotation.AddLock)")
        public void addLockAnnotationPointcut() {
    
        }
        @Around(value = "addLockAnnotationPointcut()")
        public Object addKeyMethod(ProceedingJoinPoint joinPoint) throws Throwable {
            //     
            Object proceed;
            //      
            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            AddLock addLock = AnnotationUtils.findAnnotation(method, AddLock.class);
            String logInfo = getLogInfo(joinPoint);
            //       
            String redisKey = getRediskey(joinPoint);
            logger.info("{}   ={}",logInfo,redisKey);
            //     ID;                。
            String requestId = UUID.randomUUID().toString();
            boolean lockReleased = false;
            try {
                boolean  lock = distributedLock.lock(requestId, redisKey, addLock.expireTime());
                if (!lock ) {
                    throw new RuntimeException(logInfo+":    ");
                }
                //       
                proceed = joinPoint.proceed();
                boolean releaseLock = distributedLock.releaseLock(redisKey, requestId);
                lockReleased = true;
                if(releaseLock){
                    throw new RuntimeException(logInfo+":     ");
                }
                return proceed;
            } catch (Exception exception) {
                logger.error("{}    ,key = {},message={}, exception = {}", logInfo,redisKey, exception.getMessage(), exception);
                throw exception;
            } finally {
                if(!lockReleased){
                    logger.info("{}       ={}",logInfo,redisKey);
                    boolean releaseLock = distributedLock.releaseLock(redisKey, requestId);
                    logger.info("releaseLock="+releaseLock);
                }
            }
        }
    
        /**
         *       loginfo
         *              AddLock   
         *         loginfo
         * @param joinPoint    
         * @return logInfo
         */
        private String getLogInfo(ProceedingJoinPoint joinPoint){
            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            AddLock annotation = AnnotationUtils.findAnnotation(method, AddLock.class);
            if(annotation == null){
                return methodSignature.getName();
            }
            return annotation.logInfo();
        }
        /**
         *           
         * @param joinPoint   
         * @return redisKey
         */
        private String getRediskey(ProceedingJoinPoint joinPoint) {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method targetMethod = methodSignature.getMethod();
            Object target = joinPoint.getTarget();
            Object[] arguments = joinPoint.getArgs();
            AddLock annotation = AnnotationUtils.findAnnotation(targetMethod, AddLock.class);
            String spel=null;
            if(annotation != null){
                spel = annotation.spel();
            }
            return SpelUtil.parse(target,spel, targetMethod, arguments);
        }
    }
    

    3.3実列の適用
    分散ロックとSpring明示的なトランザクション連携の問題:分散ロックとSpringトランザクションが連携して使用される場合、分散ロックでデータベーストランザクションを保護する問題があります.実行手順1.ロックを取得します.2.トランザクションを開きます.3.トランザクションのコミット、4.ロックを解除します.しかし、ステップ4でロックの解放に失敗した場合、トランザクションがロックの失効時間を超えて実行され、ロックが自動的に解放された場合、トランザクションはどのようにロールバックされますか.私が考えている解決策は、ロックの失効時間よりも小さいトランザクションの失効時間を追加することです.@Transactionのtimeoutプロパティを使用できます.ロックの失効時間よりも少ない失効時間を設定します.分散ロックが無効になる前に、トランザクションはタイムアウトされ、トランザクションがロールバックされます.
        @AddLock(spel = "'spel:'+#p0",logInfo = "      ")
        @Transactional(timeout = 5)
        public  void doWorker(String key) {}
    

    4.まとめ
    現在のredis分散ロックの実装には依然として問題があり、
  • には単一点障害の問題があり,公式に解決策としてRedLockアルゴリズムが与えられている.
  • ロックの取得に失敗した後、異常を放出するしかなく、スレッドをブロックすることはできません.Redissonオープンソースフレームワークはこれらの問題を解決した.