普通实现
说道 Redis 分布式锁大部分人都会想到: setnx+lua, 或者知道 set key value px milliseconds nx. 后一种方式的核心实现命令如下:
- 获取锁(unique_value 可以是 UUID 等)
SET resource_name unique_value NX PX 30000
- 释放锁(lua 脚本中, 一定要比较 value, 防止误解锁)
- if Redis.call("get",KEYS[1]) == ARGV[1] then
- return Redis.call("del",KEYS[1])
- else
- return 0
- end
这种实现方式有 3 大要点(也是面试概率非常高的地方):
set 命令要用
- set key value px milliseconds nx
- ;
value 要具有唯一性;
释放锁时要验证 value 值, 不能误解锁;
事实上这类琐最大的缺点就是它加锁时只作用在一个 Redis 节点上, 即使 Redis 通过 sentinel 保证高可用, 如果这个 master 节点由于某些原因发生了主从切换, 那么就会出现锁丢失的情况:
在 Redis 的 master 节点上拿到了锁;
但是这个加锁的 key 还没有同步到 slave 节点;
master 故障, 发生故障转移, slave 节点升级为 master 节点;
导致锁丢失.
正因为如此, Redis 作者 antirez 基于分布式环境下提出了一种更高级的分布式锁的实现方式: Redlock. 笔者认为, Redlock 也是 Redis 所有分布式锁实现方式中唯一能让面试官高潮的方式.
Redlock 实现
antirez 提出的 redlock 算法大概是这样的:
在 Redis 的分布式环境中, 我们假设有 N 个 Redis master. 这些节点完全互相独立, 不存在主从复制或者其他集群协调机制. 我们确保将在 N 个实例上使用与在 Redis 单实例下相同方法获取和释放锁. 现在我们假设有 5 个 Redis master 节点, 同时我们需要在 5 台服务器上面运行这些 Redis 实例, 这样保证他们不会同时都宕掉.
为了取到锁, 客户端应该执行以下操作:
获取当前 Unix 时间, 以毫秒为单位.
依次尝试从 5 个实例, 使用相同的 key 和具有唯一性的 value(例如 UUID)获取锁. 当向 Redis 请求获取锁时, 客户端应该设置一个网络连接和响应超时时间, 这个超时时间应该小于锁的失效时间. 例如你的锁自动失效时间为 10 秒, 则超时时间应该在 5-50 毫秒之间. 这样可以避免服务器端 Redis 已经挂掉的情况下, 客户端还在死死地等待响应结果. 如果服务器端没有在规定时间内响应, 客户端应该尽快尝试去另外一个 Redis 实例请求获取锁.
客户端使用当前时间减去开始获取锁时间 (步骤 1 记录的时间) 就得到获取锁使用的时间. 当且仅当从大多数 (N/2+1, 这里是 3 个节点) 的 Redis 节点都取到锁, 并且使用的时间小于锁失效时间时, 锁才算获取成功.
如果取到了锁, key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤 3 计算的结果).
如果因为某些原因, 获取锁失败(没有在至少 N/2+1 个 Redis 实例取到锁或者取锁时间已经超过了有效时间), 客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功, 防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁).
Redlock 源码
redisson 已经有对 redlock 算法封装, 接下来对其用法进行简单介绍, 并对核心源码进行分析(假设 5 个 Redis 实例).
POM 依赖
- <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
- <dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson</artifactId>
- <version>3.3.2</version>
- </dependency>
用法
首先, 我们来看一下 redission 封装的 redlock 算法实现的分布式锁用法, 非常简单, 跟重入锁 (ReentrantLock) 有点类似:
- Config config1 = new Config();
- config1.useSingleServer().setAddress("redis://192.168.0.1:5378")
- .setPassword("a123456").setDatabase(0);
- RedissonClient redissonClient1 = Redisson.create(config1);
- Config config2 = new Config();
- config2.useSingleServer().setAddress("redis://192.168.0.1:5379")
- .setPassword("a123456").setDatabase(0);
- RedissonClient redissonClient2 = Redisson.create(config2);
- Config config3 = new Config();
- config3.useSingleServer().setAddress("redis://192.168.0.1:5380")
- .setPassword("a123456").setDatabase(0);
- RedissonClient redissonClient3 = Redisson.create(config3);
- String resourceName = "REDLOCK_KEY";
- RLock lock1 = redissonClient1.getLock(resourceName);
- RLock lock2 = redissonClient2.getLock(resourceName);
- RLock lock3 = redissonClient3.getLock(resourceName);
- // 向 3 个 Redis 实例尝试加锁
- RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
- boolean isLock;
- try {
- // isLock = redLock.tryLock();
- // 500ms 拿不到锁, 就认为获取锁失败. 10000ms 即 10s 是锁失效时间.
- isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
- System.out.println("isLock ="+isLock);
- if (isLock) {
- //TODO if get lock success, do something;
- }
- } catch (Exception e) {
- } finally {
- // 无论如何, 最后都要解锁
- redLock.unlock();
- }
唯一 ID
实现分布式锁的一个非常重要的点就是 set 的 value 要具有唯一性, redisson 的 value 是怎样保证 value 的唯一性呢? 答案是 UUID+threadId. 入口在 redissonClient.getLock("REDLOCK_KEY"), 源码在 Redisson.java 和 RedissonLock.java 中:
- protected final UUID id = UUID.randomUUID();
- String getLockName(long threadId) {
- return id + ":" + threadId;
- }
获取锁
获取锁的代码为 redLock.tryLock()或者 redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS), 两者的最终核心源码都是下面这段代码, 只不过前者获取锁的默认租约时间 (leaseTime) 是 LOCK_EXPIRATION_INTERVAL_SECONDS, 即 30s:
- <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
- internalLockLeaseTime = unit.toMillis(leaseTime);
- // 获取锁时需要在 Redis 实例上执行的 lua 命令
- return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
- // 首先分布式锁的 KEY 不能存在, 如果确实不存在, 那么执行 hset 命令(hset REDLOCK_KEY uuid+threadId 1), 并通过 pexpire 设置失效时间(也是锁的租约时间)
- "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]);",
- // 这三个参数分别对应 KEYS[1],ARGV[1]和 ARGV[2]
- 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:
释放锁
释放锁的代码为 redLock.unlock(), 核心源码如下:
- protected RFuture<Boolean> unlockInnerAsync(long threadId) {
- // 释放锁时需要在 Redis 实例上执行的 lua 命令
- return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
- // 如果分布式锁 KEY 不存在, 那么向 channel 发布一条消息
- "if (redis.call('exists', KEYS[1]) == 0) then" +
- "redis.call('publish', KEYS[2], ARGV[1]);" +
- "return 1;" +
- "end;" +
- // 如果分布式锁存在, 但是 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;",
- // 这 5 个参数分别对应 KEYS[1],KEYS[2],ARGV[1],ARGV[2]和 ARGV[3]
- Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
- }
参考: https://redis.io/topics/distlock
来源: http://www.bubuko.com/infodetail-3161277.html