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();
}

二、分析

像上面的写法,符合我们的常规思维,因为对于缓存,基本都要设置一个过期时间,不能一直留在redis中,也是为了防止程序挂了,没有释放锁,所以都会设置一个过期时间

但这个过期时间,一般设置多长?

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

这时候,你会说,一般我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,简单来说,就是有个定时器,会去看我们的业务执行完没,没有就帮我们进行延时,看似没有问题吧

我们来看下源码,无论我们使用哪种方式,最终都会进到这个tryAcquireAsync方法,那就是看门狗机制的核心代码

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {if (leaseTime != -1L) {// 前面我们指定了过期时间,会进到这里,直接加锁return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 没有指定过期时间的话,默认采用LockWatchdogTimeout,默认是30sRFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);// ttlRemainingFuture执行完,添加一个监听器,类似netty的时间轮ttlRemainingFuture.addListener(new FutureListener<Long>() {public void operationComplete(Future<Long> 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<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);// 什么时候触发执行?future.addListener(new FutureListener<Boolean>() {public void operationComplete(Future<Boolean> 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指定
  • 看门狗每过 (过期时间 / 3)就去续期
  • 看门狗机制底层实现,类似Netty的时间轮


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部