当前位置:首页 > 问答 > 正文

Redis分布式锁的那些事儿,怎么管理才靠谱又高效

主要综合了互联网上多位资深开发者和架构师的实践经验分享,特别是来自美团技术团队、阿里云开发者社区以及个人技术博客中的常见讨论和解决方案)

Redis分布式锁,说白了就是在分布式系统里,大家抢着用同一个资源的时候,用来避免“打架”的一种机制,想象一下,多个服务实例就像一群人要进同一个房间办事,但一次只能进一个人,Redis分布式锁就是挂在门上的那把锁,谁拿到了钥匙谁才能进去,听起来简单,但真要把它做得靠谱又高效,里面的门道可不少。

第一件事:怎么才算一把靠谱的锁?

你得保证这把锁是“排他”的,这是最基本的,在任何一个时刻,只能有一个客户端能成功获得锁,它得能防止“死锁”,万一某个客户端拿到锁之后,还没来得及释放就因为各种原因(比如服务崩溃、网络中断)挂掉了,锁不能被它一直占着茅坑不拉屎,必须有个自动释放的机制,还得保证锁的安全性,获得锁和释放锁的必须是同一个客户端,不能让别人给误释放了。

Redis分布式锁的那些事儿,怎么管理才靠谱又高效

第二件事:用Redis实现锁,最容易踩的坑是什么?

最早很多人用SETNX(SET if Not eXists)命令来实现,想法很直接:用一个特定的键(Key)作为锁,第一个执行SETNX的客户端创建了这个键,就算拿到了锁,但这里有个大问题:如果客户端拿到锁后崩溃了,这个键就会永远存在,锁也就永远无法释放了,为了解决这个问题,人们会给锁设置一个过期时间(TTL),用EXPIRE命令,但SETNXEXPIRE是两个独立的命令,不是原子操作,万一客户端在SETNX成功之后、执行EXPIRE之前崩溃了,死锁还是会发生。

现在普遍推荐使用一条命令原子性地完成设值和设置过期时间,在Redis 2.6.12版本之后,可以直接用SET命令加上NX(不存在才设置)和PX(毫秒级过期时间)选项来实现。SET lock_key unique_value NX PX 30000,这条命令保证了只有在lock_key不存在时才会设置它,并且同时设置30秒的过期时间,完美避免了设置值和设置超时不是原子操作的问题。

Redis分布式锁的那些事儿,怎么管理才靠谱又高效

第三件事:锁的过期时间设多久?这是个头疼的问题。

设置过短,业务逻辑还没执行完,锁就自动释放了,会导致两个客户端同时进入临界区,锁就失效了,设置过长,万一客户端挂了,其他客户端要等很久才能获取到锁,系统可用性就差了,这被称为“锁超时”问题,一个常见的做法是,将过期时间设置得比业务逻辑的平均执行时间稍长一些,并配合一个后台线程,在锁还未释放时定期去“续期”(延长过期时间),Redisson这个流行的Redis客户端库就内置了这个功能,它称之为“看门狗”(Watchdog)机制。

第四件事:释放锁没那么简单,小心帮别人开了门。

Redis分布式锁的那些事儿,怎么管理才靠谱又高效

释放锁的时候,不能简单粗暴地直接DEL删除键,因为可能出现这种情况:客户端A拿到了锁,然后因为某些原因(比如发生了GC停顿)执行时间变长,超过了锁的过期时间,此时锁自动释放了,客户端B趁机拿到了锁,客户端A缓过神来,执行完了业务逻辑,然后调用DEL删除锁,这下就把客户端B刚获得的锁给释放掉了!这绝对是大事故。

释放锁的时候,必须验证这个锁是不是自己持有的,这就是为什么在上面设置锁的命令中,那个unique_value(唯一值,比如UUID)如此重要,在释放锁时,要先获取锁当前的值,判断是否与自己设置的那个唯一值相等,如果相等才能删除。“获取值”和“删除”这两个操作又不是原子的,在并发下还是可能出问题,为了解决这个原子性问题,可以使用Lua脚本,因为Lua脚本在Redis中是原子执行的,释放锁的Lua脚本大致逻辑是:if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

第五件事:怎么才能更高效?

在高并发场景下,大量客户端同时争抢一把锁,会对Redis实例造成巨大压力,一种提升效率的思路是使用“红锁”(RedLock),红锁算法的核心思想是,不再依赖单个Redis实例,而是同时向多个独立的Redis主节点申请锁,当且仅当从大多数(N/2+1)节点上都获取到了锁,并且总的耗时小于锁的过期时间,才认为获取锁成功,这提高了锁的可靠性(容忍部分节点故障),但也引入了更大的复杂性和性能开销(需要与多个节点通信),是否需要使用红锁,需要根据业务对一致性的要求来权衡,在绝大多数业务场景下,如果Redis本身做了主从高可用,上面提到的单实例锁配合合理的过期时间和续期机制,已经足够可靠和高效了。

管理好Redis分布式锁,关键是要做到几点:一是使用原子命令(SET key value NX PX timeout)来加锁,避免死锁;二是为锁设置合理的过期时间,并考虑实现续期机制应对超时;三是释放锁时使用Lua脚本验证所有权,避免误删;四是根据业务场景的容错要求,决定是使用单实例锁还是更复杂的红锁,把这些事儿都考虑周全了,这把分布式锁用起来才能既靠谱又高效。