分布式锁这话题,只要做过两年以上后端,基本都绕不开。我见过太多团队,上来就选 Redis 做分布式锁,代码写完了才被人问住:“如果 Redis 主从切换了,锁丢了怎么办?”然后就开始纠结要不要换 Etcd。也有团队一开始就上 Etcd,结果业务量根本没那么大,反而被运维折腾得够呛。
这篇文章我不想跟你谈太多“标准答案”,而是想从一个实践者的角度,把 Redis 和 Etcd 这两条路掰开揉碎,讲讲它们各自适合什么场景、有哪些坑、怎么落地、怎么排查。尤其是那些面试题里不会细讲的“生死局”——锁的安全性到底由什么决定,选型时你真正该问自己的问题是什么。
1. 分布式锁要解决的根本问题:先讲清楚需求再谈选型
很多人在选分布式锁时,第一反应是拿 Redis 和 Etcd 比性能、比功能,但我觉得这是本末倒置。分布式锁本质上是“在分布式系统里对共享资源做互斥访问”,真正的难题不是能不能加锁,而是加锁之后,系统依然要能安全运行。
1.1 一个满足生产要求的分布式锁必须具备的五个特性
先列一下我的判断标准,这是这几年做分布式系统时慢慢总结出来的。你拿任何分布式锁方案往这五条上套,基本能看出它适不适合你的业务。
一是互斥性。同一时刻,只能有一个客户端持有锁。这一条看似简单,但实现起来很容易出岔子——比如用了非原子的“先检查后设置”,多个客户端同时判断锁不存在,然后同时设置成功,互斥性就被破坏了。
二是防死锁。持锁的进程崩溃了、网络中断了、或者被某个阻塞的调用卡住了,锁必须能在有限时间内自动释放,否则整个系统等于炸了。实现上一般是给锁设置过期时间(TTL),或者依赖租约机制。
三是可重入性。同一个线程如果已经持有锁,再次请求同一把锁时应该直接成功,不然很容易发生自死锁。这个特性很多自研锁会忽略,但用到递归调用、或者一个方法里二次加锁时就麻烦了。
四是高性能和高可用。加锁和解锁的开销要尽可能小,不能因为加个锁,接口的 RT 凭空多出几十毫秒。同时锁服务本身不能是单点,Redis 得是主从加哨兵或者 Cluster,Etcd 得是 Raft 集群。
五是公平性(可选)。多个请求同时竞争锁时,能不能按照先到先得的顺序拿到锁?很多业务确实不需要公平锁,但有些场景(比如定时任务调度、分布式事务协调)如果锁长期被某个客户端霸占,不公平的锁会让一部分节点饿死。
想清楚这五个特性,你再看 Redis 和 Etcd 的方案,思路会特别清楚。很多干了几年的人对着两个方案纠结来纠结去,其实是没有把“需求”和“实现”分开。
1.2 从业务场景反推技术选型:低并发场景 vs 强一致场景
接下来,把自己代入两个不同的业务场景里感受一下。
场景 A:一个中小型电商系统,秒杀活动时库存要扣减,多个实例同时处理订单,需要保证不超卖。这个系统整体用 Redis 做缓存、做 Session、做各种消息队列,可以说 Redis 已经是基础设施了。业务对锁的要求是“快”,加锁解锁延迟必须控制在 1 毫秒以内,因为每次扣库存都要走一遍锁。至于“极端情况下锁可能丢失”,团队评估后觉得概率很低,而且就算超卖一点点也有对账系统兜底。这种场景,Redis 分布式锁就是最合适的。
场景 B:一个跨数据中心的分布式存储系统,元数据变更时需要做分布式协调,比如“哪台机器是 leader”“这个配置项只能由一台机器修改”。系统本身对数据一致性要求极高,配置写错了、leader 重复了,会直接导致数据损坏甚至整个集群不可用。这时候哪怕 Redis 锁“几乎不会丢”,团队也不敢用。他们会选 Etcd——不是因为 Etcd 性能差但可靠,而是因为 Etcd 的一致性模型在根源上就杜绝了“锁被同时持有多份”的可能性。
你有没有发现,一旦把业务场景摆出来,选型其实没有那么多纠结。不是“Redis 好还是 Etcd 好”,而是“你的业务到底能不能容忍锁失效”。这也是我今天最想传达的核心观点:选分布式锁,本质上是选一致性模型,不是选数据库。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis 分布式锁的原理与正确落地姿势
Redis 做分布式锁,网上文章多如牛毛,但很多代码是错的。这一节我会从最底层的原子操作讲起,然后到实际生产中应该怎么用,最后聊聊 RedLock 那场著名的争议。
2.1 核心原子操作:SET NX EX 为何能成为分布式锁的基础
Redis 2.6.12 版本开始,SET 命令支持了 NX 和 EX 两个参数,这才让“一行命令实现加锁”成为可能。一条命令同时完成“判断 key 是否存在”和“设置 key 及过期时间”,从 Redis 单线程命令执行模型来看,天然就是原子的。
加锁的完整命令长这样:
code复制SET lock:order:1001 8f7d2a9e-3f0c-4a1b-8c5e2d4f6a7b NX PX 30000
其中 lock:order:1001 是锁的 key,业务上用要锁定的资源 ID 来命名;NX 表示只有 key 不存在时才设置成功;PX 30000 表示锁的自动过期时间是 30 秒;value 是一串全局唯一的随机值(UUID 或者雪花 ID),这个值是用来保证“只能释放自己持有的锁”。
解锁为什么不直接用 DEL?因为如果你直接删 key,极端情况下会把自己的锁删掉别人的锁。比如线程 A 加的锁因为 GC 暂停导致过期了,线程 B 立刻加锁成功,然后 A 恢复过来删 key——直接把 B 的锁删掉了。所以解锁必须用 Lua 脚本原子地校验 value 是否匹配,匹配才删:
code复制if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这里 value 的作用也可以理解为“锁的持有者身份证”,校验通过才允许释放。这段逻辑还要再强调一下:检查和删除是两个操作,用了 Lua 把它们包裹成一个原子操作,才能真正安全。
2.2 客户端实践:Redisson / go-redis / RedisTemplate 的取舍
直接拿 jedis 封装 Lua 脚本也能用,但生产环境我建议直接用成熟客户端库。
Java 生态里,Redisson 是事实标准。它内部封装好的分布式锁用起来非常简洁,而且内置看门狗(Watchdog)机制。这个机制很关键:Redisson 默认给锁设置的过期时间是 30 秒,但这 30 秒不是固定的——只要加锁的线程还没执行完,Redisson 的后台线程就会每 10 秒给锁续期一次,把过期时间重置回 30 秒。这样就不会出现“业务还没跑完,锁先到期了”的尴尬局面。
java复制RLock lock = redissonClient.getLock("lock:order:1001");
boolean isLocked = lock.tryLock(1, 10, TimeUnit.SECONDS);
if (!isLocked) {
// 获取锁失败,直接返回或做降级
return;
}
try {
// 业务逻辑
} finally {
lock.unlock();
}
Go 生态里,go-redis 没有封装看门狗,需要自己实现续约 goroutine。这个点要注意,很多人用 go-redis 写分布式锁,只设置了过期时间,没有续约逻辑,结果长任务执行超过过期时间后锁就失效了。建议要么自己写一个带心跳的 goroutine,要么直接用 Redisson 的思路,把续约逻辑封装好。
Spring Boot 场景下,很多人直接在 service 层用 RedisTemplate 写 Lua 脚本。能用,但像可重入、续约这些能力都得自己重新造轮子,而且很容易造歪。如果不是公司明确禁止引入新依赖,我建议别走这条路。
2.3 RedLock 的争议:为什么很多人不推荐它
关于 RedLock(红锁),这算是 Redis 分布式锁话题里最热门的“坑”之一。RedLock 的思路是:在 N 个独立的 Redis 节点上依次加锁,只要超过一半节点加锁成功,就认为整体加锁成功。这样依赖多副本冗余,试图解决单节点故障导致锁丢失的问题。
但是,2016 年分布式系统领域的大佬 Martin Kleppmann(《Designing Data-Intensive Applications》的作者)发表了一篇深度批判文章,指出 RedLock 存在几个致命问题。最核心的是,RedLock 没有解决“进程 GC 暂停导致的锁过期”问题——线程 A 拿到锁后发生长时间 GC,所有 Redis 节点上的锁都过期了,线程 B 重新加锁成功并开始执行,然后 A 的 GC 结束恢复执行,两个线程就同时进入了临界区。这不是多几个副本就能解决的。
Redis 作者 Salvatore Sanfilippo(antirez)也写了一篇回应文章,两人你来我往,但最终没有一个能让所有人信服的结论。我的看法是:RedLock 里引入的“向多个独立节点写入”的思路有参考价值,但它的复杂度已经超过大多数业务能承受的范围,而且依然不能做到绝对安全。如果你的业务真的需要“绝对安全”的锁,Redis 方案本质上就不该在候选列表里。
3. Etcd 分布式锁:为什么说它是强一致场景的“标准答案”
如果你对一致性的要求到了“不容商量”的地步,Etcd 几乎是绕不开的选项。它是云原生社区的事实标准协调存储,Kubernetes 底层就依赖它做元数据存储。用它实现分布式锁,安全性在根源上就比 Redis 高一截。
3.1 三个核心概念:租约、Revision 与 Watch
要理解 Etcd 分布式锁,必须先理解三个基础概念。
第一个是租约(Lease)。Etcd 的所有 key 都可以绑定一个租约,租约有 TTL。TTL 到期后,绑定的 key 自动删除。这和 Redis 的 TTL 思路很像,但关键区别在于——Etcd 的租约是可以续期的,客户端通过 KeepAlive 接口持续向服务端发送续约请求,就能一直让自己的锁保持存活。
第二个是 Revision。这是 Etcd 内部一个全局自增的版本号,每次任何 key 的写入都会导致 Revision 加一。分布式锁就是利用 Revision 来判定加锁的先后顺序:谁创建锁 key 时的 Revision 更小,谁就排在前面。
第三个是 Watch。这是 Etcd 的监听机制,客户端可以监听某个 key 的变化。在分布式锁里,排队等待的客户端只需要 Watch 自己前一个持有者的 key,一旦前一个删除了,就立刻被通知到,然后尝试去拿锁。这种“等待-通知”模式比轮询优雅得多,也节省资源。
这三个概念组合起来,就是一个非常完善的分布式锁模型:租约负责“防死锁”,Revision 负责“公平排队”,Watch 负责“高效唤醒”。
3.2 手写一个 etcd 分布式锁(含代码与流程拆解)
Etcd 官方客户端库里面其实已经提供了 concurrency 包,里面封装了 NewSession 和 NewMutex,可以直接用。不过为了帮你理解原理,我先把“不用封装库”实现锁的完整流程讲一遍,你再去用封装库就会觉得非常简单。
加锁的流程分四步:
第一步,创建租约。比如设置 TTL 为 10 秒,然后通过 LeaseGrant 接口创建租约,获得 leaseId。
第二步,尝试创建锁 key。key 的名字可以设计成 lock/resource-id/客户端唯一标识,然后通过事务(Txn)把这个 key 绑定到租约上,设置一个较小或者较大的 revision 策略来判定成功或失败。
第三步,检查自己的 Revision 是不是最小的。Etcd 里所有客户端创建的锁 key,因为 key 名不同,都能创建成功。它们按 Revision 从小到大排序,Revision 最小的人持有锁。如果你不是最小的,用 Watch 监听比你的 Revision 小的前一个 key,等它消失。
第四步,持有锁执行完业务后,主动删除锁 key 或者撤销租约。租约撤销后,所有绑定在该租约下的 key 都会消失,等待的客户端就会被 Watch 机制唤醒。
直接用官方库就简单多了:
go复制cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"http://127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))
defer session.Close()
mu := concurrency.NewMutex(session, "/lock/order/1001")
ctx := context.TODO()
if err := mu.Lock(ctx); err != nil {
// 获取锁失败
log.Fatal(err)
}
// 业务逻辑
mu.Unlock(ctx)
这个写法里只要不忘记在业务执行期间持续执行 Session KeepAlive,锁基本不会意外丢失。即使客户端崩溃了,租约 TTL 到期后锁会自动释放,不会死锁。
3.3 Etcd 锁的安全边界:它可以容忍什么、不能容忍什么
Etcd 的方案是不是就“绝对安全”?也不是。它的安全边界在哪里,我帮你画清楚。
Etcd 的 Lease 续约依赖客户端与服务端之间持续有心跳。如果客户端发生了长时间的 GC 暂停(比如 Full GC 几十秒),或者网络分区,导致心跳丢失,租约就可能过期。锁一旦过期,其他客户端就能加锁成功,原来的客户端恢复后不知道自己已经“失去锁”了,就会造成临界区并发。
但是你会发现,这个问题在 Redis 里更隐蔽,在 Etcd 里更透明。Etcd 模式下,如果你需要更严格的保护,可以在业务代码里增加一个独立的“锁检查”点,比如在执行关键操作之前通过一个原子接口确认自己是否仍然持有锁。而在 Redis 模式下,你想做这种检查,反而没有原生的好办法。
Etcd 的另一个安全优势是:它的 Raft 协议保证了线性一致性读。意思是,你读到的数据一定是最新已提交的数据,不会出现因主从复制延迟导致读到旧数据的情况。Redis 的异步主从复制在极端场景下是会读到旧数据的,这在锁场景里是致命的。
4. 正面硬刚:Redis 和 Etcd 到底该怎么选
前面讲完了各自的原理,这一节我把它们放在天平上,从多个维度做一次全方位对比。我会尽量用表格+结论的方式来呈现,方便你直接抄作业。
4.1 一致性、可用性与可靠性的关键对比
先看一张总表:
| 对比维度 | Redis 分布式锁 | Etcd 分布式锁 |
|---|---|---|
| 一致性模型 | 最终一致(主从模式下有延迟窗口) | 线性一致(Raft 多数派确认) |
| 锁丢失风险来源 | 主从切换、进程 GC、时钟跳跃、AOF 刷盘策略 | 租约续期失败、客户端 GC |
| 加锁原理 | SET NX EX(单键原子操作) | Lease + Revision + Txn + Watch |
| 公平性 | 不保证,抢到就是赢家 | 天然公平,按 Revision 排队 |
| 典型加锁延迟 | 约 0.1~0.5ms | 约 1~5ms(取决于集群和 RTT) |
| 复杂度 | 低,Redis 已经很普及 | 中高,需管理证书、Raft 成员维护 |
| 适合场景 | 高吞吐、可容忍极小概率锁失效的业务 | 强一致要求、分布式协调、元数据互斥 |
这条表最值得关注的是“锁丢失风险来源”那一行。Redis 方案的锁丢失有四个主要来源:主从切换发生时,slave 提升为 master,旧 master 上未同步的锁 key 直接丢了;客户端进程 GC 暂停时间超过锁 TTL;服务器时钟发生大幅跳跃,导致 key 提前过期;Redis 默认 AOF 每秒刷盘,如果发生宕机,最近一秒的锁写入可能没落盘。
Etcd 方案的风险来源要少得多,核心就是租约续期失败。所以想清楚一个问题:你面对的到底是“必须处理这些风险”的业务,还是“通常不会遇到这些风险”的业务。
4.2 性能对比与资源成本的现实估算
很多团队担心 Etcd 性能不够,但实际上分布式锁对性能的消耗通常不是瓶颈。
单次加锁,Redis 是纯内存 + 单线程 O(1) 操作,在局域网内延迟大概是 0.1~0.5ms。Etcd 需要走一次 Raft 写入,leader 要把日志复制给多数派节点并收到确认,然后才能返回成功,这个 RTT 整体算下来,局域网内也要 1~5ms。看起来 Etcd 是 Redis 的 10 倍延迟,但要注意这是“绝对值”,5ms 在绝大多数业务里完全感受不到差异。
我做过压测:同一台机器、同一个网络环境,Redis 单实例做分布式锁,QPS 大概能到 10 万级别;Etcd 三节点集群做分布式锁,QPS 大概在 2.5 万到 5 万之间。你说这个差距大吗?对于绝大多数企业应用来说,哪怕峰值 5000 QPS,也根本达不到 Etcd 的上限。
资源成本层面,一个 Redis 主从加哨兵结构,轻量得不得了;Etcd 则建议至少 3 节点、奇数节点、独立的挂载盘,还要考虑证书管理。如果你是个人项目或者小团队,Etcd 的运维成本确实比 Redis 高出一截。不过现在 Etcd 的 docker 化部署、Operator 管理都比较成熟了,成本也没有想象中那么可怕。
把性能、可靠性、运维成本放一起看,我的结论是:只要你的业务不是严格到“不允许锁失效”,Redis 是更划算的选择;一旦进入严格强一致领域,加锁多出来那 1~5ms 根本不该成为你的决策变量。
4.3 那些“看起来能用”的替代方案:数据库锁、ZooKeeper
聊两个容易混淆的替代方案。
数据库分布式锁,就是建一张锁表,插入成功代表加锁成功,删除代表释放锁。它的最大问题是性能低(每次加锁都要写磁盘)和“持有锁的会话断开时无法自动释放”。后者是致命伤,甚至比 Redis 丢锁更危险。MySQL 8.0 提供的 GET_LOCK() 函数虽然有连接级自动释放能力,但在集群部署、跨库场景下依然不好用。所以数据库锁我只建议用在极低并发的内部工具上,比如迁移脚本互斥。
ZooKeeper 的分布式锁和 Etcd 思路类似,也是用临时顺序节点 + Watch 机制。ZK 曾是事实标准,但 ZooKeeper 的 ZAB 协议和 Raft 相比,写性能略逊,而且 ZK 的原子性保证在某些极端场景下不如 Etcd 简洁。现在新项目普遍倾向 Etcd,ZK 则多用于 Kafka、HBase 这类传统大数据生态里的场景。
5. 生产环境实战:分布式锁的典型故障与排查实录
最后这部分,我把这几年在生产环境踩过、见过的坑整理出来。每一段我都标了“症状”和“排查思路”,遇到类似问题可以直接对照。
5.1 故障实录:Redis 锁丢失的两个真实场景
有一次是主从切换丢锁。当时一个服务用 Redis 主从架构做分布式锁,某天主节点所在的机器发生故障,哨兵把从节点提升为主节点。切换完成后,原来主节点上还没同步到从节点的“锁 key”直接消失了,立刻有两个客户端可以同时加锁成功。业务直接出现了一个关键数据被并发修改的问题。
排查的时候,我们发现这个问题的根子在于锁 key 还没来得及通过主从复制同步到从节点就发生了 failover。解决办法是在 Redis 配置里开启 wait 1 0(WAIT 命令阻塞直到至少一个从节点确认写入)来保证写操作同步到从节点后再返回,但这样加锁延迟会明显上升。最后团队反复权衡,还是选择了接受“极小概率锁失效”,把业务代码做成了“加锁后二次校验”,配合告警和补偿任务兜底。所以说,Redis 锁不是不能在生产用,但你要知道它的屋顶在哪,并且把能兜住的都兜住。
第二个场景是关于客户端 GC 和锁过期的。某次我们给一个 Java 服务加了分布式锁保护资源,单次业务运行超过 30 秒,锁的 TTL 设的是 10 秒。结果服务一发生 Full GC,线程暂停几十秒,锁到期被自动释放,另一个线程就进来了。看起来是“锁过期了”,实际上真凶是 GC 暂停。这个问题用 Redisson 的 Watchdog 能解决大半,但自研锁或者简单封装 RedisTemplate 的代码就没有这个能力。后来我们做了一个约定:所有加锁保护的代码块,内部不允许有超过锁过期时间一半的阻塞操作,否则一定要换带自动续约的客户端。
5.2 常见问题速查表:从加锁失败到死锁排查
我整理了五个高频问题,你可以直接当成速查表用。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 加锁经常失败 | 过期时间设置太短,业务执行时间超过 TTL | 检查业务耗时,改用 Redisson 看门狗或手动续约 |
| 锁释放后其他线程迟迟拿不到锁 | Watch 或订阅机制失效 | Redis 场景用 Redis PubSub 的 Redisson 会有延迟,Etcd 场景检查 Watch 连接是否被关闭 |
| 锁莫名其妙消失了 | 主从切换/进程 GC | 检查 Redis 主从复制同步情况,检查 GC 日志,评估是否要引入 Etcd |
| 两个线程同时进入临界区 | 没有用 Lua 保证释放锁的原子性,或者 value 校验失败 | 看释放锁的代码,确认是不是用了先 get 再 del 的拆开写法 |
| 等待锁时接口超时 | 锁排队机制没有超时控制 | tryLock 要设获取等待超时时间,不能无限制阻塞 |
5.3 面试风向:分布式锁面试题里考官真正想考察的东西
这个标题上了热搜,所以我从面试角度多说几句。
分布式锁是后端面试的高频题,但很多候选人沉迷在“怎么用 Redis 实现分布式锁”,代码背得滚瓜烂熟,结果一被追问就卡住。真正有经验的面试官问这个问题,想考察的不是你会不会两条命令,而是三个层次:
第一个层次,能不能说清楚“SET NX EX 为什么是原子的”“释放锁为什么要用 Lua 脚本”。这不是填空题,而是看你有没有理解分布式并发控制的核心,即“检查-操作”必须是一个不可分割的整体。
第二个层次,能不能分析 Redis 锁的失效边界。你自己写出的方案哪里有坑?主从切换、GC 暂停、时钟跃迁,这些底层机制是如何影响锁安全性的?这里深度考察候选人对 Redis 本身的理解,而不是简单背方案。
第三个层次,能不能做技术选型判断。什么时候选 Redis,什么时候选 Etcd?这个时候我特别希望听到的回答是:“考虑业务对一致性的要求”“考虑锁失效后的影响范围”“考虑团队运维能力”,而不是“Redis 快,所以用 Redis”这种拍脑袋答案。
如果候选人能自然而然地聊到 Etcd 的 Lease、Revision、Watch 机制,我会额外加分,因为这代表他不仅仅会用一个工具,而是理解了一类问题背后的抽象模型。
写在最后的选型建议
我自己的体会是——分布式锁的选型,没有“正确方案”,只有“适合方案”。Redis 胜在极致的简单和性能,Etcd 胜在一致性和可靠性。如果你正在建设一个强一致核心系统(比如真正意义上的分布式事务、配置中心、leader 选举),直接上 Etcd,别犹豫;如果你的业务是高并发打卡、库存扣减这类可容忍极小概率超卖/重复处理的场景,Redis + Redisson 就足够好。
最后再分享一个小技巧:不管你选了哪个方案,都要提前为“持锁执行期间的异常”和“锁释放的兜底”做准备。Redis 方案最好加一层“后置校验 + 对账补偿”,Etcd 方案要监控租约续期的成功率。任何锁都不是银弹,锁只能解决并发互斥,真正让系统稳如老狗的,永远是你的降级、重试、补偿这些旁边的工作。
