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

用Redis锁别太随便,效率和坑都得提前想清楚才行

说到用Redis实现分布式锁,很多人觉得很简单,不就一个SET key value NX PX命令嘛,但真要把它用在生产环境,尤其是在稍微有点规模的系统里,可不能这么随便,用好了它能解决多台机器同时操作一个数据的难题,用不好那就是给系统埋下了一堆不知道什么时候会炸的雷,效率和可能遇到的坑,必须得提前想清楚。

(来源:在日常开发中,很多开发者对Redis锁的认知停留在基本命令的使用上)

为啥要用锁?举个例子,比如电商平台秒杀商品,库存就100件,但同时有上万个人点击购买,如果没有锁,每台处理请求的服务器都去读数据库,发现库存还有,然后都去执行减库存的操作,最后库存很可能变成负数,这就超卖了,锁的作用就是保证在同一时间,只有一台服务器的一个线程能执行这段减库存的关键代码。

(来源:分布式锁解决的典型场景是资源竞争问题,如库存扣减)

用Redis做锁,最核心的就是那个SETNX命令(现在通常直接用SET命令加上NXPX参数)。NX意思是只有当这个key不存在的时候才能设置成功,这就相当于抢到了锁。PX是给这个key设置一个过期时间,比如10秒,这样即使抢到锁的服务器因为某种原因挂掉了,没能主动释放锁,这个锁也会在10秒后自动过期,别的服务器还能继续抢,避免了死锁。

(来源:Redis官方文档中关于SET命令NX、PX参数的说明)

但这第一步“加锁”,就有好几个坑等着你,第一个坑,锁的 value 值不能随便设,有些人可能随便设个1或者固定的字符串,这会导致一个问题:服务器A抢到了锁,执行时间比较长,超过了锁的过期时间,锁自动释放了,这时服务器B成功抢到了锁,紧接着,服务器A执行完了,它要去释放锁,如果锁的value都一样,服务器A就会把服务器B刚创建的锁给删掉了!这锁就完全失效了,正确的做法是,value必须是一个全局唯一的值,比如UUID或者请求ID,在释放锁的时候,要先判断当前锁的value是不是自己设置的那个,如果是,才能删除,这个“判断+删除”的操作还必须是原子性的,得用Lua脚本来实现,不然判断完刚要删除的时候,锁可能又过期了,又会误删别人的锁。

(来源:《Redis实战》等资料中强调锁值唯一性与释放锁时的原子性操作)

第二个坑,锁的过期时间设置多久合适?设短了,业务逻辑还没执行完,锁就没了,和没加锁一样,会导致数据错乱,设长了,万一持有锁的客户端真的崩溃了,其他客户端要白白等待很长时间,系统吞吐量会急剧下降,这其实没有标准答案,你需要根据你业务代码的平均执行时间和峰值压力来评估,留出足够的余量,更高级的做法是使用“看门狗”机制,也就是另起一个线程,在业务逻辑还在执行时,定期去给锁“续期”,延长过期时间。

(来源:分布式锁的“看门狗”机制在开源框架如Redisson中有成熟实现)

第三个大坑是,你以为Redis锁是万能的,但它并不是百分百可靠,因为Redis通常采用主从或者哨兵模式保证高可用,但数据同步是异步的,想象一下这个场景:客户端A在Master节点上成功加锁,但是这个锁还没有同步到Slave节点,Master就宕机了,哨兵机制选举出一个新的Master(原来的Slave),但这个新的Master上没有客户端A的锁,这时客户端B过来,就能在新Master上成功加上锁,两个客户端同时认为自己持有锁,数据一致性就被破坏了,如果要应对这种极端情况,就需要使用Redis的Redlock算法,它要求同时向多个独立的Redis实例尝试加锁,超过半数成功才算真正拿到锁,但这套方案更复杂,性能也更低,而且业界对它还有争议。

(来源:Redis作者Antirez提出的Redlock算法及其相关争议)

再说效率问题,加锁本质上就是让并行的请求串行化,这是有性能代价的,如果你的业务场景并发量非常高,比如瞬间几万请求来抢一个商品的库存,那么所有请求都会排队,只有一个能成功,其他全部失败或者等待,这对用户体验是很大的伤害,不能所有地方都无脑加锁,很多时候可以考虑用更柔和的方式,比如把库存数量提前加载到Redis中,用decr原子命令来减库存,这样既保证了线程安全,又避免了加锁的开销,或者对一些业务进行拆分,减少对同一把锁的竞争。

(来源:高并发场景下,优化思路常包括避免大锁、使用原子操作等)

用了Redis锁,监控一定要跟上,你得清楚地知道,锁的争夺是否激烈(等待时间是否过长)、有没有出现锁过期的情况、加锁失败的比例是多少,这些指标能帮你判断锁的使用是否合理,业务逻辑是否存在瓶颈,没有监控,锁用得好不好就成了盲人摸象。

(来源:分布式系统可观测性实践中,对关键组件如分布式锁的监控是重要一环)

Redis分布式锁是一个强大的工具,但它绝不是一把简单的“万能钥匙”,从value的设置、过期时间的管理,到释放锁的原子性,再到对Redis集群架构的认知,每一个环节都藏着细节和陷阱,在决定使用它之前,一定要仔细评估你的业务场景:是不是非用锁不可?有没有更优的无锁方案?如果必须用,那么锁的粒度能不能更细一点?对可能出现的各种故障情况(客户端宕机、网络分区、Redis节点失效)要有充分的预案,别等到线上出了诡异的数据问题,才回头来补课。

用Redis锁别太随便,效率和坑都得提前想清楚才行