我们目前项目中使用的 redis 锁并没有续期的功能,所以在执行长时间任务时会触发 attempt to unlock '{}', not locked by current thread.
异常,解决这个问题比较正确的姿势是采用 redisson
这个客户端工具.具体介绍可以搜索最大的同性交友网站 github
.
我们看官方的解释:
Redisson 为避免存储这个分布式锁的Redisson节点宕机出现锁死的情况,在内部提供了一个监控锁的看门狗,其作用是在Redisson实例被关闭前,不断延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒,可以通过修改 Config.lockWatchdogTimeout
来指定。另外Redisson还通过加锁的方法提供了 leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
细心的同学可能会发现上面解释的歧义:
看门狗每隔 30 秒检查一次锁的超时时间
看门狗会去检查锁的超时时间,锁的时间时间默认是30秒
所以看门狗具体是怎么检查锁的,我们从源码中找答案。 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static void main (String[] args) { Config config = new Config (); config.useSingleServer().setAddress("redis://127.0.0.1:6379" ); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("anyLock" ); lock.lock(); }public RLock getLock (String name) { return new RedissonLock (connectionManager.getCommandExecutor(), name); } public RedissonLock (CommandAsyncExecutor commandExecutor, String name) { super (commandExecutor, name); this .commandExecutor = commandExecutor; this .id = commandExecutor.getConnectionManager().getId(); this .internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(); this .entryName = this .id + ":" + name; this .pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub(); }private long lockWatchdogTimeout = 30 * 1000 ;
上面代码我们看到锁的释放时间就是看门狗的超时时间,默认为30秒,但还有一个问题,看门狗是多久来延长一次有效期呢?我们接着往下看:
继续看 lock()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 public void lock () { try { lock(-1 , null , false ); } catch (InterruptedException e) { throw new IllegalStateException (); } }private void lock (long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId = Thread.currentThread().getId(); Long ttl = tryAcquire(leaseTime, unit, threadId); if (ttl == null ) { return ; } ... }private Long tryAcquire (long leaseTime, TimeUnit unit, long threadId) { return get(tryAcquireAsync(leaseTime, unit, threadId)); }private <T> RFuture<Long> tryAcquireAsync (long leaseTime, TimeUnit unit, long threadId) { ... RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e != null ) { return ; } if (ttlRemaining == null ) { scheduleExpirationRenewal(threadId); } }); return ttlRemainingFuture; } private void scheduleExpirationRenewal (long threadId) { ExpirationEntry entry = new ExpirationEntry (); ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry); if (oldEntry != null ) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); renewExpiration(); } } private void renewExpiration () { ... Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask () { @Override public void run (Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null ) { return ; } Long threadId = ent.getFirstThreadId(); if (threadId == null ) { return ; } RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null ) { log.error("Can't update lock " + getName() + " expiration" , e); return ; } if (res) { renewExpiration(); } }); } }, internalLockLeaseTime / 3 , TimeUnit.MILLISECONDS); }
从上面分析我们就知道了,获取锁成功就会开启一个定时任务,也就是 watchdog
,定时任务会定期检查去续期 scheduleExpirationRenewal(threadId)
. 这里定时用的是netty-common包中的 HashedWheelTimer
,默认情况下,该定时调度每次调用的时间差是 internalLockLeaseTime / 3
.也就10秒.
总结: 通过源码分析我们知道,默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了.