Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 等。
redis分布式的常规实现
Redis是最常见的实现分布式锁的方法之一,而很多人都了解使用了redis分布式锁使用redis的
SET key value [EX seconds] [PX milliseconds] [NX|XX]
指令。
对于删除操作,由于先判断key是否存在,然后执行del操作,为避免删除其他线程生成的锁,因此需要执行使用lua脚本执行,保证原子性,脚本如下
-- lua删除锁:-- KEYS和ARGV分别是以集合方式传入的参数 key为锁名称,argv为每个锁的唯一表示if redis.call('get', KEYS[1]) == ARGV[1] then -- 执行删除操作 return redis.call('del', KEYS[1]) else -- 不成功,返回0 return 0end
超时时间怎么设置
上面实现的过程中,我们对于设置key的超时时间的设置怎么处理?一旦我们设置太短,业务代码耗时过长,则会被超时释放;如果设置的太长,万一线程获得锁后线程异常或死亡,未能正常释放锁,容易导致业务积压。
有人说我们可以预先计算业务耗时,设置一个最长的,其实这个看似很合理,但实际上,作为一款分布式锁的基础服务,本应与业务脱离,需要保证未来业务场景,同时简化使用方式。
其实,redisson就针对这样的问题提供了解决方案,那就是watch dog(看门狗),下面我们看下redisson的源码。
watch dog即通过开启一个线程进行key的续期操作。
先看redisson的demo使用,如下
// 初始化Config config = new Config();// 由于本地使用单机模式,其他模式也config.useSingleServer().setAddress("127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);// 获取锁RLock lock = redisson.getLock("lockkey");lock.lock(30, TimeUnit.SECONDS);
那么我们直接从redisson.getLock去往下跟踪代码,为了简化,我们直接看到最后的watch dog的代码(一步一步跟源码,肯定会看到这段代码)
private RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { // 获取锁 return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } else { // watchdog RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining == null) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } }
此时我们看核心代码tryLockInnerAsync
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, //如果锁不存在,创建锁,并返回空 "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; //如果锁存在,且value匹配,说明是当前线程持有的锁,对key增量加1(重入次数+1),返回空 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; //申请锁失败,返回锁的剩余时间 return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); }
上面可以看到如果有锁会返回nil,然后会看到最上面的代码,会判断,如果为null,会进行scheduleExpirationRenewal方法调用,如下
ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining == null) { this.scheduleExpirationRenewal(threadId); } } });
scheduleExpirationRenewal方法内容如下:
protected RFuture renewExpirationAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // 如果有锁则续期 30s(源码中初始化 this.lockWatchdogTimeout = 30000L) "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); }
此时我们终于看到了续期操作,了解了watch dog的原理。