• Redis分布式锁这样用,有坑?


    背景

    在微服务项目中,大家都会去使用到分布式锁,一般也是使用Redis去实现,使用RedisTemplate、Redisson、RedisLockRegistry都行,公司的项目中,使用的是Redisson,一般你会怎么用?看看下面的代码,是不是就是你的写法

    String lockKey = "forlan_lock_" + serviceId;
    RLock lock = redissonClient.getLock(lockKey);
    
    // 方式1
    try {
    	lock.lock(5, TimeUnit.SECONDS);
    	// 执行业务
    	...
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	// 释放锁
    	lock.unlock();
    }
    
    // 方式2
    try {
    	if (lock.tryLock(5, 5, TimeUnit.SECONDS)) {
    		// 获得锁执行业务
    		...
    	}
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	// 释放锁
    	lock.unlock();
    }
    

    分析

    像上面的写法,符合我们的常规思维,一般,为了避免程序挂了的情况,没有释放锁,都会设置一个过期时间
    但这个过期时间,一般设置多长?

    设置过短,会导致我们的业务还没有执行完,锁就释放了,其它线程拿到锁,重复执行业务
    设置过长,如果程序挂了,需要等待比较长的时间,锁才释放,占用资源

    这时候,你会说,一般我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,简单来说,就是有个定时器,会去看我们的业务执行完没,没有就帮我们进行延时,看似没有问题吧,那我们来简单看下源码,无论我们使用哪种方式,最终都会进到这个方法,就是看门狗机制的核心代码

    private  RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
        if (leaseTime != -1L) {
            // 前面我们指定了过期时间,会进到这里,直接加锁
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            // 没有指定过期时间的话,默认采用LockWatchdogTimeout,默认是30s
            RFuture ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            // ttlRemainingFuture执行完,添加一个监听器,类似netty的时间轮
            ttlRemainingFuture.addListener(new FutureListener() {
                public void operationComplete(Future future) throws Exception {
                    if (future.isSuccess()) {
                        Long ttlRemaining = (Long)future.getNow();
                        if (ttlRemaining == null) {
                            RedissonLock.this.scheduleExpirationRenewal(threadId);
                        }
                    }
                }
            });
            return ttlRemainingFuture;
        }
    
    

    scheduleExpirationRenewal方法

    private void scheduleExpirationRenewal(final long threadId) {
     if (!expirationRenewalMap.containsKey(this.getEntryName())) {
         Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
             public void run(Timeout timeout) throws Exception {
             	 // renewExpirationAsync就是执行续期的方法
                 RFuture future = RedissonLock.this.renewExpirationAsync(threadId);
                 // 什么时候触发执行?
                 future.addListener(new FutureListener() {
                     public void operationComplete(Future future) throws Exception {
                         RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                         if (!future.isSuccess()) {
                             RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
                         } else {
                             if ((Boolean)future.getNow()) {
                                 RedissonLock.this.scheduleExpirationRenewal(threadId);
                             }
    
                         }
                     }
                 });
             }
         }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 当跑了LockWatchdogTimeout的1/3时间就会去执行续期
         if (expirationRenewalMap.putIfAbsent(this.getEntryName(), new RedissonLock.ExpirationEntry(threadId, task)) != null) {
             task.cancel();
         }
     }
    

    所以,结论是啥?

    // 方式1
    lock.lock(5, TimeUnit.SECONDS);
    // 方式2
    lock.tryLock(5, 5, TimeUnit.SECONDS)
    

    我们这两种写法都会导致看门狗机制失效,如果业务执行超过5s,就会出问题

    解决

    正确的写法应该是,不指定过期时间

    // 方式1
    lock.lock();
    // 方式2
    lock.tryLock(5, -1, TimeUnit.SECONDS)
    

    你可以会觉得不妥,不指定的话,就默认按照30s续期时间,然后每10s去看看有没有执行完,没有就续期,
    我们也可以指定续期时间,比如指定为15s

    config.setLockWatchdogTimeout(15000L);
    

    总结

    • 在使用Redisson实现分布式锁,不应该设置过期时间
    • 看门狗默认续期时间是30s,可以通过setLockWatchdogTimeout指定
    • 看门狗会每internalLockLeaseTime / 3L去续期
    • 看门狗底层实际就是类似Netty的时间轮
  • 相关阅读:
    【计算机网络】TCP 协议的相关特性
    对基本类型-整型数据结构的认识
    算法通过村第八关-树(深度优先)白银笔记|深度和高度问题
    vue @click点击事件不生效
    Centos7下zabbix安装与部署,设置中文(保姆级图文)【网络工程】
    Nginx 面试 40 问与答
    vim命令编辑完文件后,按ESC键退出编辑模式,无法进入命令模式解决方案
    android12.0(S) 通知栏不显示闹钟和静音图标 bug
    .net----委托和事件
    Win10添加、删除鼠标右击的选项(快捷方法)
  • 原文地址:https://www.cnblogs.com/huozhonghun/p/17323627.html