Redis分布式锁
1、单机版分布式锁
SET key value[EX seconds][PX milliseconds][NX|XX]
key | 标志位 |
value | 唯一值,自己只能释放自己的锁 |
EX seconds | 设置过期时间,单位为秒 |
PX milliseconds | 设置过期时间,单位毫秒 |
NX | 仅当key不存在时设置值 |
XX | 仅当key存在设置值 |
public boolean tryLock(String key, String uniqueId, int seconds) { SetParams setParams = new SetParams(); setParams.ex(seconds); setParams.nx(); return "OK".equals(jedis.set(key, uniqueId, setParams)); }
uniqueId必须为唯一,解决的场景是:
- 客户端A获取锁成功
- 客户端A业务逻辑执行时间>失效时间,锁被释放
- 客户端B获取锁成功
- 客户端A业务执行完毕,释放掉B的锁。
- 加上唯一value之后,删除锁先比较value防止删除错误锁
public boolean deleteLock(String key, String value) { String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " + "return redis.call('del',KEYS[1]) else return 0 end"; return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L); }
2、问题
这类缺点是加锁操作只作用在一个redis节点上,即使通过sentinel保证高可用,还是可能出现锁丢失,如以下情况:
- 客户端在Redis的master获取锁成功
- 加锁的key还未同步到salve
- master failover
- salve节点升级为master
- 锁丢失,多个客户端获取到锁
3、RedLock
- 有N个Redis Master,这些节点完成独立,我们在N个Master上使用相同方法获取锁和释放锁。
- 假设有五个Redis Master节点,我们需要在五台服务器上分别运行,可以保证他们不会同时宕机
- 为了获取锁,客户端要执行以下操作:
- 获取当前Unix时间,以毫秒为单位
- 依次尝试从5个实例,使用相同的key和具有唯一性的value来获取锁。
- 客户端设置一个获取锁的超时时间,该超时时间应该远小于key的失效时间,如果超过获取锁时间,那么马上向下一个Redis Mater获取锁
- 客户端使用当前时间 - 第一步获取的时间,就是获取锁消耗的时间
- 如果5/2+1的节点都获取到锁,并且获取锁消耗的时间小于key的失效时间,那么锁获取成功
- key的真正有效时间 = 失效时间 - 获取锁的时间
- 如果因为某些原因,获取锁失败,客户端向在所有Redis Master上进行解锁
3.1、Redisson RedLock
redisson已经有对redlock算法封装
public void tryLock() { Config config = new Config(); config.useSentinelServers() .addSentinelAddress("127.0.0.1:6369", "127.0.0.1:6379", "127.0.0.1:6389") .setMasterName("masterName") .setPassword("password") .setDatabase(0); RedissonClient redissonClient = Redisson.create(config); RLock redLock = redissonClient.getLock("REDLOCK_KEY"); boolean isLock; try { //500ms获取锁的超时时间,10000ms是锁失效时间 isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS); } catch (Exception e) { } finally { redLock.unlock(); } }
实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId
protected String getLockName(long threadId) { return id + ":" + threadId; }
获取锁
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { internalLockLeaseTime = unit.toMillis(leaseTime); return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//首先分布式锁key不存在,执行hset命令,并设置过期时间 "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; " +
//如果key存在,并且value匹配,那么重入次数+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; " +
// 获取分布式锁的KEY的失效时间毫秒数 "return redis.call('pttl', KEYS[1]);", Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); }
-
KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;
-
ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;
-
ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:
释放锁
protected RFuture<Boolean> unlockInnerAsync(long threadId) { return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回 "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " +
// 如果就是当前线程占有分布式锁,那么将重入次数减1 "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
//重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除 "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "else " +
//重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息 "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; "+ "end; " + "return nil;", Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)); }