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

打印 上一主题 下一主题

主题 1781|帖子 1781|积分 5343

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
背景

在微服务项目中,大家都会去使用到分布式锁,一般也是使用Redis去实现,使用RedisTemplate、Redisson、RedisLockRegistry都行,公司的项目中,使用的是Redisson,一般你会怎么用?看看下面的代码,是不是就是你的写法
  1. String lockKey = "forlan_lock_" + serviceId;
  2. RLock lock = redissonClient.getLock(lockKey);
  3. // 方式1
  4. try {
  5.         lock.lock(5, TimeUnit.SECONDS);
  6.         // 执行业务
  7.         ...
  8. } catch (Exception e) {
  9.         e.printStackTrace();
  10. } finally {
  11.         // 释放锁
  12.         lock.unlock();
  13. }
  14. // 方式2
  15. try {
  16.         if (lock.tryLock(5, 5, TimeUnit.SECONDS)) {
  17.                 // 获得锁执行业务
  18.                 ...
  19.         }
  20. } catch (Exception e) {
  21.         e.printStackTrace();
  22. } finally {
  23.         // 释放锁
  24.         lock.unlock();
  25. }
复制代码
分析

像上面的写法,符合我们的常规思维,一般,为了避免程序挂了的情况,没有释放锁,都会设置一个过期时间
但这个过期时间,一般设置多长?
设置过短,会导致我们的业务还没有执行完,锁就释放了,其它线程拿到锁,重复执行业务
设置过长,如果程序挂了,需要等待比较长的时间,锁才释放,占用资源
这时候,你会说,一般我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,简单来说,就是有个定时器,会去看我们的业务执行完没,没有就帮我们进行延时,看似没有问题吧,那我们来简单看下源码,无论我们使用哪种方式,最终都会进到这个方法,就是看门狗机制的核心代码
  1. private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
  2.     if (leaseTime != -1L) {
  3.         // 前面我们指定了过期时间,会进到这里,直接加锁
  4.         return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
  5.     } else {
  6.         // 没有指定过期时间的话,默认采用LockWatchdogTimeout,默认是30s
  7.         RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  8.         // ttlRemainingFuture执行完,添加一个监听器,类似netty的时间轮
  9.         ttlRemainingFuture.addListener(new FutureListener<Long>() {
  10.             public void operationComplete(Future<Long> future) throws Exception {
  11.                 if (future.isSuccess()) {
  12.                     Long ttlRemaining = (Long)future.getNow();
  13.                     if (ttlRemaining == null) {
  14.                         RedissonLock.this.scheduleExpirationRenewal(threadId);
  15.                     }
  16.                 }
  17.             }
  18.         });
  19.         return ttlRemainingFuture;
  20.     }
复制代码
scheduleExpirationRenewal方法
  1. private void scheduleExpirationRenewal(final long threadId) {
  2. if (!expirationRenewalMap.containsKey(this.getEntryName())) {
  3.      Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
  4.          public void run(Timeout timeout) throws Exception {
  5.                   // renewExpirationAsync就是执行续期的方法
  6.              RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
  7.              // 什么时候触发执行?
  8.              future.addListener(new FutureListener<Boolean>() {
  9.                  public void operationComplete(Future<Boolean> future) throws Exception {
  10.                      RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
  11.                      if (!future.isSuccess()) {
  12.                          RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
  13.                      } else {
  14.                          if ((Boolean)future.getNow()) {
  15.                              RedissonLock.this.scheduleExpirationRenewal(threadId);
  16.                          }
  17.                      }
  18.                  }
  19.              });
  20.          }
  21.      }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 当跑了LockWatchdogTimeout的1/3时间就会去执行续期
  22.      if (expirationRenewalMap.putIfAbsent(this.getEntryName(), new RedissonLock.ExpirationEntry(threadId, task)) != null) {
  23.          task.cancel();
  24.      }
  25. }
复制代码
所以,结论是啥?
  1. // 方式1
  2. lock.lock(5, TimeUnit.SECONDS);
  3. // 方式2
  4. lock.tryLock(5, 5, TimeUnit.SECONDS)
复制代码
我们这两种写法都会导致看门狗机制失效,如果业务执行超过5s,就会出问题
解决

正确的写法应该是,不指定过期时间
  1. // 方式1
  2. lock.lock();
  3. // 方式2
  4. lock.tryLock(5, -1, TimeUnit.SECONDS)
复制代码
你可以会觉得不妥,不指定的话,就默认按照30s续期时间,然后每10s去看看有没有执行完,没有就续期,
我们也可以指定续期时间,比如指定为15s
  1. config.setLockWatchdogTimeout(15000L);
复制代码
总结


  • 在使用Redisson实现分布式锁,不应该设置过期时间
  • 看门狗默认续期时间是30s,可以通过setLockWatchdogTimeout指定
  • 看门狗会每internalLockLeaseTime / 3L去续期
  • 看门狗底层实际就是类似Netty的时间轮

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表