redis分布式锁和2个房客的故事你听过吗

程序员书单

共 2799字,需浏览 6分钟

 · 2021-01-26


为什么需要分布式锁这里就不赘述了。常见的分布式锁实现方案有Redis、Zookeeper,数据库。

设计一个分布式锁,至少应该保证以下3个方面:

  1. 安全: 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
  2. 无死锁:即便是天塌下来,也要锁能释放。
  3. 容错。只要大部分Redis节点都活着,客户端就可以获取和释放锁。

redis部署方案一般有这3种。

  1. 单机模式

  2. master-slave + sentinel

  3. 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中文网。我总结的大概流程是

  1. 依次尝试从N个实例获取锁,注意的是获取锁的时间要远小于锁超时的时间。
  2. 当且仅当从大多数(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一些细节。最后揭秘《俩个房客的故事》纯属本人虚构,如有雷同,天理不容。


浏览 17
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报