我们都知道可以使用Redis实现一个分布式锁。
加锁时:

1
2
3
// 一条命令保证原子性执行
127.0.0.1:6379> SET lock $uuid EX 10 NX
OK

解锁时,使用lua脚本保证原子性:

1
2
3
4
5
6
7
// 判断锁是自己的,才释放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end

但是这样的方案解决不了锁过期的问题,也就是说如果加锁的进程在解锁之前,锁已经到了过期时间的话,锁就会被释放,从而其他进程就可以获取到锁。分布式锁的互斥性就被打破了。
解决这个问题的思路就是续租。加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。这个方案已经有了好的实现,Redisson
上述的解决方案在单实例Redis中可以很好地发挥作用,如果使用的是Redis集群的话,就会有问题。比如由于Redis的复制是异步的,当master上的锁还没有来的及同步到其他副本上,此时发生主从切换,就会出现锁丢失的情况。
针对这种情况,Redis的作者提出了Redlock方案。

Redlock

在Redis的分布式环境中,我们假设有N个Redis Master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制
为了取到锁,客户端应该执行以下操作:

  1. 获取当前时间戳T1,以毫秒为单位。
  2. 依次尝试从N个Master实例使用相同的key和随机值获取锁。当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间T2-T1就得到获取锁使用的时间。当且仅当从大多数的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

同样地,Redisson也提供了实现。

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
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:6379")
.setPassword("afeiblog").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.2:6379")
.setPassword("afeiblog").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.3:6379")
.setPassword("afeiblog").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "LOCK_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();
}

总结

可以看到,Redlock是依赖各个Redis的时钟保持一致。所以这一方案提出后也受到了很多人的质疑,Redis的作者也做出了相应的回应。具体过程很复杂,可以参考参考链接的内容。
除了Redis之外,我们还可以使用Zookeeper实现分布式锁,它主要依靠临时节点断开连接自动删除的特性以及watch机制实现。但是性能上还不如Redis。

在我们正常的部署场景中,一般只需要一个Redis Cluster,或者一个主从复制 + Sentinel。而这两者都不能承载RedLock的落地,RedLock方案需要专门搭建一套环境。所以,如果不是对分布式锁可靠性有极高的要求(比如金融场景),不太建议使用RedLock方案。

所以大部分场景,我都会使用单机版 Redis实现分布式锁。

参考