用Redis搞读写锁,这方案挺新鲜也实用,聊聊它怎么同步的
- 问答
- 2025-12-29 18:13:35
- 6
主要基于Redis官方文档中对分布式锁的经典实现模式,即Redlock算法的基础思想,并结合了常见的读写锁应用场景进行阐述,同时也参考了社区中对于Redis分布式锁的实践讨论。)
聊到用Redis搞读写锁,这事儿确实挺新鲜也挺实用的,咱们先别被“分布式锁”、“读写锁”这些词吓到,说白了,它的核心目标很简单:在多台机器、多个服务同时想操作同一份数据的时候,能像交通警察一样,指挥谁先走、谁后走,别撞车,特别是区分开“读”和“写”这两种操作,因为读操作通常可以大家一起读,但写操作必须得独占,不能一边写一边读,也不能同时有两个人写,否则数据就乱套了。
Redis这个内存中的键值数据库,为啥能担此重任呢?关键就在于它速度快、命令原子性,以及一些好用的特性,它就像一个超级快的公告板,大家都往上面贴条子、看条子,通过条子的内容来达成默契和同步。
它是怎么“锁”住的?核心机制
想象一下,我们要保护一个叫“用户A的余额”的数据。

-
写锁(独占锁): 当一个服务(比如服务1)想要修改这个余额时,它得先申请“写锁”,怎么申请呢?它就在Redis里设置一个特殊的键值对,比如键是
lock:userA_balance,值是一个全局唯一的标识(比如UUID),并且给这个键设置一个过期时间(比如10秒)。- 关键命令:
SET lock:userA_balance <unique_id> NX PX 10000,这个命令的妙处在于NX,意思是只有这个键不存在的时候我才能设置成功,如果设置成功了,Redis会返回OK,服务1就成功拿到了写锁,可以安心地去修改余额了,如果另一个服务(服务2)也来执行同样的命令,因为键已经存在,它会失败,就知道锁被别人占着,它就得等待或者放弃。 - 同步点: 这个
SET ... NX ...命令就是第一个核心的同步点,所有想写的服务,都必须通过“抢”这个唯一的钥匙来获得权限,Redis的原子性保证了同一时刻只有一个服务能“抢”成功。
- 关键命令:
-
读锁(共享锁): 读锁就稍微复杂一点了,目标是允许多个读操作同时进行,但要阻止写操作。
- 一种常见的思路是: 用计数器,当有服务想来读数据时,它不去抢那个唯一的写锁,而是去增加一个“读锁计数器”,设置一个键
read_lock:userA_balance,初始值为0,每个读操作进来,就通过INCR命令把这个计数器加1,当计数器从0变成1的时候,除了增加计数器,第一个读操作还需要去设置一个“写锁正在被等待”的标记(比如另一个键write_waiting:userA_balance设为1),这个标记是用来告诉想写的人:“现在有人在读,你等等”。 - 同步点: 读锁的同步体现在两个地方:一是通过
INCR命令原子性地增加读者数量;二是第一个读者和最后一个读者对“写等待”标记的操作,当读操作完成时,通过DECR减少计数器,当计数器减回0时,最后一个读操作需要清除那个“写等待”标记,这样等待中的写操作就知道“现在没人读了,我可以尝试去获取写锁了”。
- 一种常见的思路是: 用计数器,当有服务想来读数据时,它不去抢那个唯一的写锁,而是去增加一个“读锁计数器”,设置一个键
读写锁之间的同步博弈
现在来看看读和写之间是怎么互相等待、避免冲突的。

-
写锁请求到来时:
- 它首先会检查是否有“写锁”已经存在(检查
lock:userA_balance),如果有,说明别的写操作在进行,它就得等。 - 如果当前没有写锁,它还会去检查“读锁计数器”
read_lock:userA_balance是否大于0,如果大于0,说明现在有读操作正在进行,这时,写操作不能立即执行,它需要设置一个“写等待”标记,然后不断地(或者通过订阅发布机制)轮询检查读计数器,直到它变为0,才能去尝试获取写锁。
- 它首先会检查是否有“写锁”已经存在(检查
-
读锁请求到来时:
- 它首先会检查是否有“写锁”存在,如果有,说明有写操作在进行,读操作必须等待写锁释放。
- 它还会检查是否有“写等待”标记,即使当前没有写锁,但如果已经有写操作在排队等待了(“写等待”标记已设置),一些严格的实现可能会让后续的读操作也排队,以防止写操作被“饿死”(即一直有读操作进来,导致写操作永远无法执行),这是一种公平性的考量,如果实现得简单点,可能读操作会直接增加计数器开始读。
保证安全的关键细节
光有上面的逻辑还不够,分布式环境很复杂,必须考虑各种意外。

-
锁的过期时间: 这是Redis分布式锁的“生命线”,一定要给锁设置过期时间!为什么呢?如果某个服务拿到锁之后,因为网络问题或者自己崩溃了,没有来得及释放锁,那么其他所有服务都会永远等下去,系统就卡死了,设置了过期时间(比如10秒),即使锁的主人挂了,10秒后锁也会自动消失,其他服务可以继续工作,这引入了“锁失效”的风险,但这是为了保证可用性必须做的权衡。
-
释放锁的安全性: 服务1拿到锁后,执行完业务逻辑,需要释放锁(删除那个键),这里有个大坑:不能直接删除
lock:userA_balance这个键,万一服务1因为某些操作比较慢,锁都快过期了才执行完,这时另一个服务2可能已经拿到了新锁,如果服务1贸然删除,删掉的其实是服务2的锁!删除前必须验证:这个锁的值是不是我当初设置的那个唯一ID?只有是“我”的锁,“我”才能删,这通常通过Lua脚本实现,因为Lua脚本在Redis中执行是原子性的,可以保证“获取键值-对比-删除”一系列操作一气呵成,中间不会被打断。 -
避免“惊群效应”: 当写锁释放时,可能有很多个读操作和写操作都在等待,如果简单地让所有等待者同时去抢,会对Redis造成压力,更优雅的方式是使用Redis的发布订阅(Pub/Sub) 功能,等待锁的服务可以订阅一个频道,当锁被释放时,释放锁的服务向这个频道发布一个消息,等待者收到消息后,再按顺序(比如先来后到)去尝试获取锁,这样更有序,压力也更小。
总结一下
用Redis搞读写锁,本质上就是利用Redis单线程执行命令的原子特性和高速的读写能力,作为一个中央的“协调者”,通过精心设计键的设置、检查、删除逻辑,并配合计数器、过期时间、发布订阅等机制,来模拟出“读共享、写独占”的同步效果,它不是一个现成的命令,而是一套基于Redis基础命令构建的“最佳实践”方案。
这个方案的优势是性能高、实现相对简单,非常适合对一致性要求不是极端苛刻(比如不是金融核心交易)但需要高并发的场景,它的挑战在于需要处理好网络延迟、服务器时钟漂移(如果用到复杂算法的话)、以及上面提到的锁失效等边界情况,理解了它底层的同步逻辑,就能更好地使用它,也能在出问题时快速定位。
本文由酒紫萱于2025-12-29发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/70794.html
