redis分布式锁和2个房客的故事你听过吗
共 2799字,需浏览 6分钟
·
2021-01-26 12:57
为什么需要分布式锁这里就不赘述了。常见的分布式锁实现方案有Redis、Zookeeper,数据库。
设计一个分布式锁,至少应该保证以下3个方面:
安全: 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。 无死锁:即便是天塌下来,也要锁能释放。 容错。只要大部分Redis节点都活着,客户端就可以获取和释放锁。
redis部署方案一般有这3种。
单机模式
master-slave + sentinel
redis cluster模式
我们看这3种如何实现分布式锁。
单机模式:正常情况下,单机模式没什么大问题,就怕万一redis挂了就完蛋了。一般我们不会采用单机版,这里之所以提到它,是因为单机版的锁是其他方案的基础。redis锁的命令是:
SET resource_name my_value NX PX ms
//NX是指如果key不存在就就返回true,key存在返回false,PX可以指定过期时间
如果简单的setnx,有些场景下会有问题。
《俩个房客的故事》 long long a ago,有A、B俩个房客前往同一家酒店,又都看上了同一间房,但是一间房同时只能容纳一个人,经过一番舌枪唇战,A抢到了第一次。A预计自己半个小时能完事,于是就开了半个小时的钟点房。可是A这次发挥超常,30分钟还没完事,但是由于30分钟时间已经到了,房间锁自动打开(房间空置状态),可以接下一位客人。这时B来了,进入房间,锁上门。正准备干事的时候,A干完事出去的时候把门打开了(释放锁),房间变成空置状态。A和B都很尴尬。
怎么办?为了不让B打扰,A想到了一个办法,让秘书A1每隔10分钟就把钟点房的时间重新设置成30分钟。等A事干完了再开门(释放锁)。
为了避免A开了B上的锁,酒店想出了一个办法,指纹上锁,开锁的时候也必须用这个指纹。这样A就开不了B上的锁。
回到程序,比如A来setnx,默认过期时间30秒,获取到锁,但是A比较墨迹,锁过期(自动释放锁)的时候还在执行,这时候B获取锁。等A执行完来释放锁的时候,其实释放的是B的锁。这个时候就需要多一层判断。A和B,set的my_value必须不一样(参考故事中的指纹)。当A释放锁的时候先判断是否是自己的锁,如果是自己的锁再释放。可以通过以下Lua脚本实现:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
也可以在自己的业务中实现,关键点就是my_value必须唯一,能区分开A和B。
至于A的问题,我们另起一个线程,来监控A的过期时间,每隔10秒钟就把A的锁过期时间设置成30秒,直到A释放锁。
主从模式,单机版有单点故障,那master-slave应该没问题了吧。master挂了slave顶上。但是请注意,master与slave之间数据同步是异步的。就是说master挂了的时候,可能有写数据并没有同步到slave。这时slave成为master的时候还是丢了锁。比如A获取到某资源的锁,这时master挂了,恰巧锁还没同步到slave。这时slave晋升为master的时候并没有A的锁,这时B过来获取资源锁的时候就成功。安全性得不到保障。当然如果访问量小这个模式完全够了,哪有那么巧的事。即便是正好赶上,由于访问量小,弥补也比较容易。
如果访问量大,对安全要求高,就得另寻出路。redis官方给出了一个解决算法。就是Redlock算法。它要求有N组(台)redis互相独立的节点,它们互相独立,没有主从,也没有集群。客户端要做的是(以5台服务为例):
1. 获取当前Unix时间,以毫秒为单位。
2. 依次尝试从N个实例,使用相同的key和随机值获取锁。当然向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1 )的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁。
上面这段来源redis中文网。我总结的大概流程是
依次尝试从N个实例获取锁,注意的是获取锁的时间要远小于锁超时的时间。 当且仅当从大多数(N/2+1 )的Redis节点都取到锁才算成功,否则就是失败。
只有当N/2+1个节点取到锁才是算成功。释放锁比较简单,就是释放每个节点的锁。
当然Redlock解决了分布式锁的基本问题,别忘了它也有《俩个房客的故事》中的问题,解决方案和单机版的redis基本一样。我们可以基于redis-client原生api来实现Redlock算法,也可以用一些框架,比如Redisson。
Redisson实现Redlock。语法就比较简单
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed2.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
rLock.lock();
try {
//搞事情....
} finally {
rLock.unlock();
}
总结
我们介绍了redis锁的一些基本要求,和常见问题,以及解决方案。当然还有其他的问题,望大家一起讨论。至于Redlock的实现,建议用框架,可以少care一些细节。最后揭秘《俩个房客的故事》纯属本人虚构,如有雷同,天理不容。