Skip to content

多台机器抢着干同一件事,怎么保证只有一个成功?—— 分布式锁 ​

属于 S2 Redis 深入 · 第四篇 上一篇:缓存问题与一致性 下一篇:面试题集

单机程序里,一把 sync.Mutex 就能保证临界区只有一个线程执行。但服务部署成多台机器后,进程之间的锁互相看不见,就需要一把跨机器的锁——分布式锁。Redis 是最常用的实现手段之一,但它有几个坑,踩对了才靠谱。

最基础的坑:加锁和设过期不是原子的 ​

很多人这样写:先 SETNX key value 加锁,再 EXPIRE key ttl 设过期。问题在于这是两步——如果加锁成功后进程崩溃了,EXPIRE 没执行,这把锁就永远不释放,成了死锁。

正确姿势是一条命令同时完成加锁和设过期:

go
SET key value NX EX 30   // NX 表示不存在才设置,EX 表示 30 秒过期

value 要放一个唯一标识(比如 UUID + 线程号),这样解锁时才能判断"这把锁是不是我的"。

解锁也要原子:为什么必须用 Lua ​

解锁的逻辑是"先判断 value 是不是我的,是才删"。如果拆成 GET + DEL 两步,中间可能出问题:你的锁恰好过期了,别人拿到了锁,你再去 DEL 就把别人的锁删了。

所以解锁要用 Lua 脚本,让"判断 + 删除"原子执行:

lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

锁过期了业务没做完:watchdog 续期 ​

设了过期时间,但业务执行超过了 TTL,锁提前释放,另一个实例就进来了。解决办法是看门狗(watchdog):加锁后起一个后台协程,在过期前自动续期,业务完成再停止。需要可重入的话,就在 value 里记"持有者 + 重入次数"。

Redlock 的争议:Redis 锁真的可靠吗 ​

上面这些只解决了"单机 Redis 的正确用法"。但 Redis 主从是异步复制的——主节点刚给你加了锁,还没同步到从节点就宕机了,从节点提升为主,锁就丢了。Redlock 的思路是向多个独立 Redis 节点加锁,过半成功才算成功。

但 Redlock 有争议(Martin Kleppmann 等指出):它依赖时钟,时钟跳跃会破坏锁的判断;而且即使过半节点成功,也无法绝对保证互斥(GC 停顿、网络延迟都会让锁在业务还在跑时过期)。

结论是:Redis 锁是"性能优先、可容忍极小概率并发"的选择;对一致性要求极高的场景(如金融),应该用基于共识的 etcd 或 ZooKeeper(有租约、强一致)。

Redis 还能做这些 ​

除了锁,Redis 在高并发场景里还有几个常用角色:限流(令牌桶/滑动窗口)、布隆过滤器(防穿透)、分布式 ID(INCR 原子自增)、排行榜(zset)、计数(点赞/阅读量 INCR + 异步落库)。这些都用它"单线程 + 原子操作 + 数据结构丰富"的特性。


串起来 ​

分布式锁的核心难点不在"怎么加锁",而在边界情况:加锁过期要原子、解锁判断删除要原子、业务超时要续期、主从切换要防丢锁。把这些边界都处理了,才是一把生产可用的锁。而 Redis 锁的"弱一致"属性,决定了它适合性能优先、容忍极小概率并发的场景。

下一篇是 S2 的面试题集,把 Redis 的高频问题收口。

持续学习,持续构建。