文章目录
- 分布式锁的基本要求
- 单机版:SET NX PX 一行搞定
- 释放锁要用 Lua
- 锁过期的另一个问题:业务超时
- 集群版的 RedLock 算法
- RedLock 的争议
- Redisson:开箱即用的方案
- 常见坑点
- 1. 锁粒度太大
- 2. 锁粒度太小
- 3. 忘了 unique_value
- 4. 没有重试机制
- 5. 用普通 SET 代替 SETNX
- 实践建议
在分布式系统里,多个进程需要协调访问共享资源时,分布式锁几乎是绕不开的工具。Redis 因为性能好、部署简单、命令丰富,成为实现分布式锁的最常见选择。但简单不等于容易——一把"看起来能用"的 Redis 分布式锁,真正用到生产环境,常常因为细节问题踩坑。
分布式锁的基本要求
任何一把分布式锁,都必须满足三个性质:
- 互斥性:同一时刻只有一个客户端能持有锁。
- 死锁避免:持有锁的客户端崩溃,锁要能自动释放。
- 解铃还须系铃人:客户端只能释放自己持有的锁,不能误删别人的。
Redis 实现分布式锁的难点,就在这三点上。
单机版:SET NX PX 一行搞定
最基础的实现:
SET lock_key unique_value NX PX30000三个关键参数:
- NX:key 不存在才设置,保证互斥。
- PX 30000:30 秒后自动过期,防止死锁。
- unique_value:客户端唯一标识,释放时校验。
加锁成功返回OK,失败返回nil。
释放锁要用 Lua
释放锁不是简单的DEL。考虑这个场景:
客户端 A 加锁,PX=30s 客户端 A 业务执行 35s(GC 卡顿等原因) 锁过期,客户端 B 加锁成功 客户端 A 完成业务,DEL lock_key ← 删的是 B 的锁!要避免这个问题,释放时必须先校验持有者。但"GET + DEL"是两条命令,中间可能被打断。所以释放必须用 Lua 脚本保证原子性:
ifredis.call('GET',KEYS[1])==ARGV[1]thenreturnredis.call('DEL',KEYS[1])elsereturn0end锁过期的另一个问题:业务超时
PX 30s 是个两难的选择:
- 太短:业务还没做完就过期,锁失效。
- 太长:进程崩溃后,锁很久才能释放,影响其他客户端。
成熟的方案是引入"看门狗"(watchdog)机制:客户端启一个后台线程,定期延长锁的过期时间。Redisson 就是这么实现的——默认每 10 秒续期一次,把锁的过期时间重置回 30 秒。这样只要持有者还活着,锁就不会过期;一旦持有者挂了,看门狗也停了,锁会自然过期。
集群版的 RedLock 算法
单机 Redis 实现的锁有个根本性问题:主库挂了。如果加锁后主库还没把锁同步到从库就崩溃,哨兵切换到从库,新主库上根本没这个锁,互斥性就被打破了。
Redis 作者 Antirez 提出了 RedLock 算法,思路是:
- 部署 N 个独立的 Redis 实例(推荐 5 个),互不主从。
- 客户端依次向所有实例申请同一个锁,记录开始时间。
- 如果超过 N/2+1 个实例加锁成功,且总耗时小于锁的过期时间,则认为加锁成功。
- 如果失败,向所有实例发送释放请求(不管之前加锁是否成功)。
只要多数派实例存活且没被网络隔离,锁就能保持互斥性。
RedLock 的争议
分布式系统专家 Martin Kleppmann 写过一篇著名文章质疑 RedLock:在 GC 暂停、时钟漂移等场景下,RedLock 仍然不能保证安全性。Antirez 也回应过,但这个争论至今没有定论。
实际工程上的建议:
- 普通业务:单机 Redis + 短 TTL + 看门狗(如 Redisson 默认实现)已经足够,简单可靠。
- 金融级强一致:直接用 ZooKeeper 或 etcd,它们的设计就是为分布式协调而生。
- 真要用 RedLock:考虑清楚是不是真的需要这种复杂度。
Redisson:开箱即用的方案
自己实现分布式锁很容易踩坑。Java 生态里 Redisson 是事实标准,封装好了所有细节:
RedissonClientredisson=Redisson.create(config);RLocklock=redisson.getLock("myLock");try{lock.lock();// 阻塞获取,自动续期// 业务逻辑}finally{lock.unlock();}Redisson 提供了:
- 自动续期(看门狗)
- 可重入(同一线程多次获取不阻塞)
- 公平锁(按申请顺序授予)
- 读写锁、信号量、CountDownLatch 等更复杂的同步原语
- 集群模式下的 RedLock 实现
Python 用redis-py自带的redis.lock,Go 用redsync,都能省掉不少基础工作。
常见坑点
1. 锁粒度太大
把整个业务都放在锁里,锁的持有时间过长,吞吐量直线下降。锁应该只保护真正有竞争的那段代码。
2. 锁粒度太小
为了优化性能把锁拆得太细,结果加多把锁反而引入死锁风险。粒度要适中。
3. 忘了 unique_value
释放锁时不校验持有者,可能误删别人的锁。这是最常见的隐藏 bug。
4. 没有重试机制
加锁失败直接报错,对业务不友好。一般要带退避重试,但要设上限避免无限等待。
5. 用普通 SET 代替 SETNX
SET key value会无条件覆盖,根本没有互斥语义。必须带 NX。
实践建议
- 能不用锁就不用,优先考虑用 Redis 原子操作或乐观锁解决。
- 生产环境用成熟库(Redisson / redis-py 内置 lock),别自己造轮子。
- TTL 要合理,业务正常耗时的 2-3 倍即可,配合看门狗续期。
- 释放锁必须用 Lua,校验持有者后再删除。
- 强一致场景换工具,ZooKeeper / etcd 比 Redis 更适合做分布式协调。
- 监控锁的等待时间,等待时间长说明竞争激烈,要么优化业务,要么调整锁粒度。
分布式锁是个看起来简单实际复杂的话题。Redis 提供的命令很基础,但要把锁用对,必须考虑过期、续期、释放、容错等一系列问题。理解这些细节,才能在真实业务里写出经得起考验的代码。