如何用Redis实现分布式锁以及可用性
如何用 Redis 实现分布式锁以及可用性
在实际开发场景中,不同客户端可能需要互斥地访问某个共享资源,即同一时刻只允许一个客户端操作该资源。为实现这一目的,通常采用分布式锁。目前流行的分布式锁实现方式包括数据库、Memcached、Redis、文件系统以及 ZooKeeper 等。其中,Redis 因其高性能和部署简单而被广泛采用。本文将分享如何利用 Redis 实现分布式锁及其可用性方案。
一、可靠分布式锁的核心要求
一个可靠且高可用的分布式锁需要满足以下几点核心要求:
- 互斥性(Mutual Exclusion):任意时刻只能有一个客户端拥有锁,不能被多个客户端同时获取。
- 安全性(Safety):锁只能被持有该锁的客户端删除,不能被其它客户端误删。
- 死锁预防(Deadlock Prevention):若获取锁的客户端因宕机等原因未能释放锁,需要有机制避免其它客户端无法获取锁(例如设置自动过期)。
- 高可用(High Availability):当部分节点宕机时,客户端仍能正常获取或释放锁。
二、基于单节点 Redis 实现分布式锁
利用单节点 Redis 实现分布式锁是最常用的方式。虽然未考虑高可用,但其实现简单、成本低廉,被很多中小型企业采用。
1. 加锁命令的选择
早期很多文章建议使用 setnx 命令实现分布式锁,但 setnx 无法原子性地设置锁的过期时间。若执行 setnx 后客户端宕机,未能设置过期时间,将导致死锁。
从 Redis 2.6.12 版本开始,set 命令完全可以替代 setnx,支持原子性地设置值和过期时间。官方 set 命令参数如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]参数说明:
EX second:设置键的过期时间为秒。SET key value EX second效果等同于SETEX key second value。PX millisecond:设置键的过期时间为毫秒。SET key value PX millisecond效果等同于PSETEX key millisecond value。NX:只在键不存在时,才对键进行设置操作。SET key value NX效果等同于SETNX key value。XX:只在键已经存在时,才对键进行设置操作。
示例:
SET key value NX PX 30000该命令表示:只有当 key 不存在时才设置值(互斥性),并将超时时间设为 30000 毫秒(防止死锁)。这满足了互斥性和死锁预防两个要求。
2. 如何满足安全性要求
问题场景:
客户端 A 拿到锁并设置过期时间为 10 秒。若客户端 A 业务执行超过 10 秒,锁自动过期,客户端 B 拿到锁。此时客户端 A 执行完毕删除锁,误删了客户端 B 的锁。
解决方案:
- 方案一:守护线程续期(Watchdog)
获得锁的线程开启一个守护线程,负责给锁“续期”。例如锁过期时间为 10 秒,守护线程在第 9 秒执行expire指令续期 10 秒。若客户端正常执行完毕,显式关闭守护线程;若客户端宕机,守护线程随之停止,锁到期后自动释放。 - 方案二:唯一标识 + Lua 脚本
加锁时将value设置为唯一标识(如 UUID),标识锁的持有者。删除锁时,先判断当前锁的 value 是否与自己的标识一致。
注意:判断和删除是两个独立操作,非原子性。因此需要使用 Lua 脚本 将判断和释放锁的操作合并执行,保证原子性。
三、提高 Redis 分布式锁的高可用性
在大型应用中,Redis 服务通常以集群形式存在(主从复制、Cluster 等)。由于 Slave 同步 Master 是异步的,可能出现以下问题:客户端 A 在 Master 上加锁,此时 Master 宕机,Slave 尚未同步锁数据即晋升为新的 Master,导致客户端 B 也能成功加锁,破坏了互斥性。
1. Redlock 算法
官方提出了 Redlock 算法来解决单点故障问题。该算法假设我们有 N 个完全独立的 Redis Master 节点(不使用复制或其他隐含的分布式协调算法)。
注意:若采用 Redis Cluster 集群,此方案可能不适用,因为 Cluster 是按哈希槽(hash slot)分配节点,存在分布式协调逻辑。
算法步骤(假设 N=5):
- 获取当前时间(单位:毫秒)。
- 轮流用相同的 key 和随机值(客户端唯一标识)在 N 个节点上请求锁。每个节点请求锁时设置一个较小的超时时间(如 5-50 毫秒),防止在宕掉节点上阻塞过久。
- 客户端计算获取锁消耗的时间。只有当在大多数节点(如 5 个中的 3 个)成功获取锁,且总消耗时间不超过锁释放时间,才认为获取成功。
- 若获取成功,锁的实际有效时间 = 最初设定的释放时间 - 获取锁消耗的时间。
- 若获取失败(成功节点不超过半数或超时),客户端向所有节点发起释放锁操作。
2. 极端场景与应对
虽然 Redlock 解决了单点问题,但若集群节点发生崩溃重启,仍可能出现安全性问题。
场景示例:
- 客户端 1 成功锁住节点 A, B, C(获取成功)。
- 节点 C 崩溃重启,锁数据丢失(未持久化)。
- 节点 C 重启后,客户端 2 锁住节点 C, D, E(获取成功)。
结果:客户端 1 和 2 同时获得了锁。
解决方案:
让 Redis 崩溃后延迟重启,且延迟时间大于锁的过期时间。这样等节点重启后,所有节点上的旧锁均已失效,避免冲突。
3. 多语言实现参考
以下是各种语言实现 Redlock 算法的开源库,供深入学习参考:
- Python: Redlock-py
- PHP: Redlock-php
- PHP (完整实现): PHPRedisMutex
- Go: Redsync.go
- Java: Redisson
- Perl: Redis::DistLock
- C++: Redlock-cpp
- C#/.NET: Redlock-cs
- NodeJS: node-redlock (支持锁续期)
四、实际生产环境中的考量
上述方案主要针对专门搭建的 Redis 集群。在实际生产中,Redis 可能只是一个“黑盒”链接(如云托管服务),程序无法感知内部的主从切换及节点拓扑。
若要实现上述高可用分布式锁,通常需要 DBA 的配合支持,以获取准确的集群状态信息。因此,选择单机方案还是高可用集群方案,需根据实际业务场景和资源情况权衡。
说明:文中提到的SET命令支持NX/EX参数特性自 Redis 2.6.12 版本引入,当前主流版本均支持。Redlock 算法在分布式社区存在一定争议,实际使用时建议结合业务容忍度评估,或考虑使用基于 ZooKeeper 等强一致性组件的锁方案。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/ru-he-yong-redis-shi-xian-fen-bu-shi-suo-yi-ji-ke-yong-xing.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。