☰
从线上事故到分布式锁服务:Redis实现与治理实战
2026/9/26 23:46:43 网站建设 项目流程

1. 一个线上事故让我开始认真对待分布式锁

我接手过一个订单系统,业务不算复杂,峰值也就几千QPS,但线上每隔一段时间就会出现一次“重复下单”的客诉。一开始我们以为是前端按钮没做防抖,后来发现前端确实做了,但依然有漏网之鱼。排查到最后,问题出在一个很不起眼的地方:用户在极短时间内提交了两次下单请求,两个请求被负载均衡分发到了两台不同的应用实例上,每个实例都跑着各自的本地锁,锁了个寂寞。

如果你还没有遇到过类似的事,那说明你的服务要么是单实例部署,要么是并发量还没到临界点。但只要你上了多实例、上了微服务,本地锁(比如synchronized、ReentrantLock)就会立刻失效——它只锁得住当前JVM进程,锁不住别的机器上的另一个进程。

什么是分布式锁?用一句话说就是:让多个进程之间互相排斥地访问同一个共享资源,把“进程内互斥”升级成“跨进程互斥”。它的核心使用场景其实很聚焦,我列一下我实际经历过的,大家可以对照自己的业务看:

  • 定时任务防重:K8s里同一个任务可能拉起多个副本,到点全跑,必须保证只有一台机器真正执行。这是分布式锁最典型的应用场景,没有之一。
  • 库存扣减与防止超卖:用户请求进来,先锁库存,再执行扣减逻辑,避免两个请求同时读到库存还剩1件。
  • 幂等性控制:同一笔支付回调可能到达多次,用锁保证只有第一次请求能真正执行后续流程。
  • 缓存击穿防护:热点key失效瞬间,大量请求同时打到DB。与其用复杂的“互斥重建”逻辑,不如直接让这些请求抢一把锁,抢到的去查DB重建缓存,没抢到的先返回旧值或短暂等待。
  • 分布式事务中的资源协调:多个微服务需要通过某一共享资源完成全局协调,比如文件处理、分布式ID生成、任务队列消费。

如果你只是单机应用、单库单表、用户量不大,说实话分布式锁对你来说大概率是个伪需求。但如果你已经在做微服务、容器化部署,那么“进程内锁”的局限性就会像定时炸弹一样埋在你的系统里,不知道哪天会在什么流量下爆掉。我自己的经验是:当你的应用第一次出现两个实例同时跑定时任务的场景出现时,就是你认真考虑分布式锁的时候。

在动手实现之前,心里必须有一张需求清单。我做分布式锁服务时,列的验收标准是这几条:

  1. 互斥性:任意时刻,同一个锁key只能被一个客户端持有。
  2. 可重入性:同一线程可以重复获取同一把锁,不至于自己把自己卡死。
  3. 防死锁:客户端崩溃、网络异常后,锁必须能自动释放,不能永久占用。
  4. 高性能:加锁/解锁整体开销要极低,不能成为业务链路的性能瓶颈。
  5. 高可用:锁服务本身不能成为单点,依赖的存储组件要能容忍故障。
  6. 可观测性:至少要能看到谁在抢锁、持锁多久、是否发生死锁或超时。

后面的所有实现方案、技术选型、坑和优化,本质上都是在向这六条标准靠拢。

2. 锁的三条技术路线:Redis、ZooKeeper与数据库的取舍

网上讲分布式锁的文章特别多,但多数是把方案列一下,很少有人认真讲清楚“为什么在这个业务场景下选这个方案”。我先把我调研过的三条路线放在一起对比,再讲我的取舍逻辑。

方案核心实现优点缺点典型应用场景
RedisSET NX EX或 Redisson性能极好、实现成熟、生态完善主从切换时可能丢锁;过期时间设置需要经验高并发、低延迟要求的业务场景
ZooKeeper临时顺序节点 + Watch强一致、天然防死锁(会话失效即释放)性能相对差、运维成本高、客户端较重对一致性要求极高的场景,如分布式协调
数据库唯一索引 /for update实现简单、不用引入新组件性能最差、可能拖垮数据库低频、内部管理类任务,小团队极简方案

2.1 Redis方案的本质是“过期时间兜底”

Redis做分布式锁的原理,简单到一句话就能说清:多进程争抢同一个key,谁SET成功谁就拿到了锁,锁的过期时间就是最长占用时间。

它最大的优势也是最大的隐患都来自同一个点——锁依赖“过期时间”来自动释放。如果业务执行时间超过了预设的过期时间,锁会被强行释放,别的客户端就能拿到锁,造成并发冲突;如果设置的过期时间太长,客户端崩了之后锁又要等很久才能自动释放。这个时间窗口怎么估,是Redis方案里最考验经验的地方。

2.2 ZooKeeper方案是“会话级锁”

ZooKeeper做锁的思路和Redis完全不同。它依赖的是ZAB协议带来的强一致性:多个客户端同时去ZooKeeper创建临时顺序节点,谁创建的节点序号最小谁就拿到锁,其他人监听比自己小的前一个节点,前一个节点删除后唤醒自己。

这里的关键是“临时节点”。临时节点的生命周期和客户端会话绑定,客户端崩了,会话断开,节点自动消失,锁自动释放——不需要像Redis那样操心过期时间,这是它相比Redis最省心的地方。代价是每次加锁解锁都要多次网络往返,性能大概比Redis差一个量级,还要维护一套ZK集群。

2.3 数据库方案是“兜底选项”

数据库做分布式锁,最常见的是利用唯一索引约束:插入一条带业务标识的记录,插入成功就是拿到锁,删除记录就是释放锁。还有一种做法是SELECT ... FOR UPDATE行锁。好处是真没有额外组件成本,坏处是性能差、数据库连接被长事务占用、一旦忘记删记录就会留下“死锁数据”,需要人工介入。适合那种一天跑不了几次的内部任务,拿来兜底可以,扛流量不行。

2.4 我的选型逻辑

我们团队最终选的是Redis作为主实现,数据库作为极端情况下的兜底。原因不复杂:

  • 我们的核心诉求是“高并发 + 低延迟 + 现有基础设施里已经有Redis集群”,引入ZK要额外养一套运维,团队规模不划算。
  • 我们的业务场景里,绝大部分锁的持有时间都在100ms以内,Redis的过期时间兜底完全够用。
  • 我们愿意在“锁丢失”的小概率事件上做补偿——比如幂等表和状态机校验,而不是让锁本身承担100%的职责。

如果你做的业务对一致性要求极其严格(比如金融级的资金操作),我建议直接用ZooKeeper或者etcd,不要纠结。在分布式系统里,最终的一致性依赖往往是“锁 + 业务自身的幂等校验”共同完成的,锁只是第一道防线。

3. Redis实现的核心原理:从SETNX到看门狗

很多人一提到Redis分布式锁就条件反射说“SETNX”,但这其实是比较古早的做法了。现在的Redis分布式锁,成熟方案应该长这样。

3.1 为什么不能分两步:SETNX再加EXPIRE

远古版本的Redis没有原子命令,大家是用SETNX拿到锁之后再单独调EXPIRE设置过期时间。这里有个致命问题:如果SETNX成功之后、EXPIRE执行之前,客户端崩了,锁就永远不释放,只能用运维脚本手工清理,非常被动。

所以Redis官方后来提供了原子操作:SET key value NX EX,一条命令同时完成“设置锁 + 过期时间”。注意这里必须用同一命令或Lua脚本保证原子性,任何分两步的实现都应该判为不合格设计。

3.2 value不能随便填,必须带唯一标识

这也是新手最容易忽略的点。很多人写:

SET lock_key "1" NX EX 30

然后释放的时候直接DEL lock_key。表面看没什么问题,实际上存在一个锁误删的经典坑:

  1. 线程A拿到锁,执行任务,但因为某个原因阻塞了,超过了30秒过期时间。
  2. 锁自动过期,线程B拿到锁,开始执行任务。
  3. 线程A终于执行完,调用DEL lock_key,直接把线程B的锁删掉了。
  4. 线程C顺势拿到锁,B和C同时执行——互斥性彻底失效。

解法是:value存一个当前线程/请求维度的唯一标识(UUID就行),释放锁时先比较value是否匹配,匹配才删。而“比较+删除”这两个动作也要保证原子性,用Lua脚本是最好的方式:

-- 释放锁的Lua脚本 if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

这样就能保证:只有锁的持有者才能释放锁,释放前还会确认锁的value仍然是自己当初写入的。

3.3 锁的过期时间怎么设

过期时间需要结合业务执行时间来评估。我们内部有个经验法则:先分析99.9%情况下的最大执行耗时,然后在这个值基础上乘5到10倍。比如一个任务正常情况下几十毫秒跑完,极端情况可能到100ms,那锁的过期时间可以设1秒,留足余量。

但这里有个矛盾:如果余量留得大,客户端崩了,锁要过很久才能被自动清理;如果余量留得小,业务稍微慢一点锁就提前过期,互斥性被破坏。所以更进阶的方案是用看门狗机制。

简单说就是:拿到锁之后,启动一个后台守护线程每隔一段时间自动给锁续期,业务没执行完,锁就永远不会过期;业务执行完释放锁时,把守护线程停掉。Redisson框架的“看门狗”正是这个思路,默认锁的超时时间是30秒,每10秒自动续期一次,续期操作也是通过Lua脚本判断value是否匹配再重置过期时间。

用Redisson的话,核心逻辑其实非常简洁:

RLock lock = redissonClient.getLock("order:create:10086"); // 尝试拿锁,最多等3秒,锁自动过期时间是30秒 boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS); if (locked) { try { // 业务逻辑 } finally { lock.unlock(); } }

坦白说,如果你要做生产级的Redis分布式锁,我强烈建议直接用Redisson,不要自己造轮子。自己实现一套看门狗需要考虑守护线程的并发安全、续期失败的降级处理、锁释放和续期的竞争条件,这些坑Redisson都已经踩平了。自己做一遍是为了理解原理,生产使用还是用成熟轮子稳妥。

3.4 可重入锁也是Redisson内置的

可重入的意思是同一个线程可以重复获取同一把锁。Redisson的可重入实现思路是:在value里记录持锁线程的信息和持有次数,每重入一次加1,每释放一次减1,减到0才真正删key。自己实现的话需要用Hash结构来做,比如:

lock_key = { "thread-123": 2 }

加锁时对Hash里的计数字段做INCR,释放时DECR,判断计数归零再删除。

3.5 主从切换时锁会丢,这是Redis方案最深的坑

Redis主从复制是异步的,存在一个理论上的时间窗口:主节点刚写入锁,还没来得及把数据复制到从节点就宕机了,从节点被提升为主节点——锁没了。此时另一个客户端可以轻松拿到同一把锁,互斥性被打破。

这个问题的经典解法是RedLock算法:同时向多个独立的Redis节点(通常5个)写锁,成功写入半数以上就算加锁成功。它的逻辑类似“过半数同意”,设计上是为了降低单点故障带来的锁丢失概率。

但我必须说清楚RedLock的争议。业界(包括Redis作者自己和其他分布式系统专家)对RedLock吵了很多年,核心分歧是:它引入了复杂度,却并没有从理论上彻底解决分布式锁在“网络分区、时钟跳跃”下的安全性问题。很多团队在生产中用了RedLock也可能没有真正模拟过节点宕机的极端场景。

我的建议是分为两种看待:

  • 如果业务允许极端情况下出现极小概率的锁失效,单Redis + Redisson足够,没必要上RedLock。
  • 如果业务要求锁绝对不能出现两个客户端同时持有(比如资金类的业务),那不要用Redis方案,直接换ZooKeeper或etcd。Redis加锁再花哨,也不如一个强一致组件让你睡得安稳。

4. 把锁做成服务:API设计、可观测性与治理

标题是“分布式锁服务实现”,光讲Redisson怎么用显然不够。接下来我想重点聊聊:如何把一个锁工具升级成一套可以长期维护、可接入多个业务方、有治理能力的服务。这部分是我在实战中摸索出来的,网上很少成体系地讲。

4.1 为什么不能只提供一个工具类

很多团队一开始做分布式锁的方式是:把Redisson封装成一个静态工具类,谁要用谁就调。这个阶段其实挺爽的,代码少、接入快。但业务多了以后问题会集中爆发:

  • 各业务方自定义了各种各样的锁时间参数,有的设5秒,有的设1分钟,没有统一规范。
  • 想统计“谁持有锁最久”“哪些锁发生频繁等待”时,没有任何数据来源。
  • 对锁key的命名风格全凭个人习惯,日志里根本对不上号。
  • 一旦锁服务的配置需要调整,比如换Redis连接,所有接入方都要重新发布。

所以我把它做成了一层独立服务。注意,这个“服务”不一定是一个独立部署的进程,更常见的是一个独立的基础组件SDK + 一份统一配置中心里的配置,对外提供标准API,内部屏蔽锁实现细节。

4.2 API设计要满足80%的业务场景

我设计的API分四类,基本上能覆盖日常所有需求:

第一类:普通同步锁

LockResult lock(LockParam param);

LockParam包含三个核心参数:锁key、等待时间、持有时间。等待时间表示抢不到锁最多等多久,持有时间表示业务执行的最大时长。

public class LockParam { private String key; // 获取锁最长等待时间,超时直接返回失败 private Duration waitTime; // 锁自动过期时间 private Duration leaseTime; private String businessId; // 业务标识,用于可观测性 }

第二类:允许抢不到就立刻失败,适合非核心链路

LockResult tryLock(LockParam param);

不等待,抢不到就直接返回失败。适合那种“拿不到锁就跳过本次执行”的场景。

第三类:异步锁

CompletableFuture<LockResult> lockAsync(LockParam param);

适合本来就用CompletableFuture编排逻辑的代码,避免阻塞线程。

第四类:手动解锁

void unlock(String lockToken);

注意这里有个设计细节:unlock的核心是lockToken而不是锁key,因为锁key可以被后续抢到锁的客户端持有,你如果只凭key去解锁,极大概率会误删别人的锁。lockToken就是前面讲的唯一标识,由服务生成并返回给调用方,调用方存好,释放时带过来。

4.3 锁的可观测性比锁本身更重要

我见过不少团队把“分布式锁能跑通”当作目标,其实真正的分水岭在于:你能不能说清楚当前系统里每一把锁的状态。

我在设计服务时,加了三层可观测性数据:

  • 基础指标:锁获取次数、获取失败次数、平均等待时间、平均持有时间、锁过期自动释放次数。这些全部通过Prometheus的Counter和Histogram采集,用Grafana画趋势面板。
  • 结构化日志:每次加锁/解锁都输出一条结构化日志,包含锁key、业务ID、客户端IP、等待时间、持有时间、释放原因(主动释放或被动过期)。这是排查问题最快的手段。
  • 链路追踪:把锁的获取和释放作为两个Span写入链路追踪系统(比如OpenTelemetry/SkyWalking),这样整个请求的调用链里能看到“在哪个环节等待了多久锁”。

说句题外话,可观测性建设大概率会帮你发现很多意料之外的怪问题。比如我们曾经通过监控发现某个核心锁的平均持有时间从10ms突然飙到2秒,最后定位到是缓存热key引起的STW问题。如果没有数据,这类问题根本无从谈起。

4.4 治理能力:超时降级、锁分组与热key优化

把锁做成服务之后,治理能力才有施展空间。我列举几个在实操中效果显著的治理策略:

超时降级:当Redis本身出现性能抖动时,锁服务的获取时间会变长。我设计了一个动态开关:当Redis的延迟P99超过阈值时,自动把非核心业务的“等待获取锁”改成“直接失败降级”,避免所有线程阻塞在锁获取上,引发线程池枯竭。核心业务则保留等待,但会记录降级日志。

锁分组:不同业务接入时按组配置,比如“交易组”“任务调度组”“营销组”。各组可以有独立的Redis实例、独立的锁过期时间规范,互不影响。这样某个组的锁出问题时,爆炸半径被控制在组内。

热点key优化:曹操有一句我很认同的话——“锁的粒度和并发度是矛盾的”。如果一把锁锁的key太粗,比如整个用户维度只有一把锁,那么所有针对该用户的操作都要排队。我在服务层提供了一种“分片锁”能力:把原来的user:123拆成user:123:shard1到user:123:shard10,按用户ID取模选择其中一把。这样同一个用户的并发热点操作可以并行执行一部分,同时又不至于完全失去互斥性。注意,分片锁会降低互斥强度,必须结合业务容忍度谨慎使用。

4.5 部署形态:SDK中心化还是Sidecar模式

锁服务的部署形态也是一个值得说的话题。有两种主流路线:

第一种是纯SDK模式:所有业务方引入SDK,SDK直接连Redis,配置从配置中心下发。优点是性能最好(少一跳网络),定位问题相对容易。缺点是新版本SDK需要业务方升级发布。

第二种是独立服务(Sidecar/微服务)模式:业务方通过RPC调用锁服务,锁服务统一管理Redis连接和锁逻辑。优点是逻辑收敛、多语言接入简单,缺点是每加一次锁就多一次RPC,性能损耗比较明显,对于高频锁场景可能无法接受。

我在实践中选择的是纯SDK模式 + 统一配置中心。锁的高频操作决定了它不能走远程RPC,否则一次业务调用要多消耗0.5ms以上的网络开销。让SDK直连Redis,锁服务只负责下发配置和收集监控数据,感觉是对的折中。

5. 高可用与脑裂:RedLock争议和ZooKeeper方案剖析

很多面试题里会问Redis分布式锁有什么缺点,应试答案一般是“主从切换可能丢锁”。但实际搞生产系统,你需要理解得更深一层:丢锁只是表象,背后的本质是分布式系统里的“时间”和“共识”两个难题。

5.1 时间难题:锁过期时间依赖服务器时钟

Redis的EXPIRE、TTL这些操作依赖的是服务器本地时钟。如果Redis服务器时钟发生跳跃(比如NTP同步出现了时间跳变,或者服务器时钟被回拨),已经设置的过期时间也会出现异常。

极端案例:加锁时设了30秒过期,结果服务器时钟往前跳了1分钟,锁“立刻”过期了。这个情况确实比较罕见,但一旦发生,就会引发和新主从切换类似的并发问题。规避办法通常是:使用Redisson的看门狗机制把过期时间不断刷新,减少对单点时钟的依赖;以及监控服务器时钟偏移,发现异常及时告警。

5.2 RedLock:过半数加锁能救Redis吗

RedLock的加锁过程是这样的:

  1. 记录当前时间,依次向5个独立Redis节点发送SET NX EX命令。
  2. 如果成功写入节点数 >= 3,且整个过程消耗的时间小于锁的过期时间,视为加锁成功。
  3. 加锁成功后启动看门狗续期。
  4. 如果加锁失败,依次删除所有节点上的锁。

它在设计上的确把“单点故障丢锁”的概率降到了很低,但也引入了新的复杂度:客户端需要同时管理多个Redis连接,网络分区下的行为变得难以推理。最经典的批评是:如果一个客户端在加锁过程中发生了长时间的GC停顿,它持有的锁可能已经过期,但其他客户端永远无法从时间戳上判断这一点。

我个人的态度是:绝大多数业务根本不需要RedLock。如果你的业务是“丢一把锁最多导致重复执行一次任务,但任务本身有幂等性兜底”,那这完全不是问题。只有“严格不能并发执行”的业务才需要考虑,而这种业务我会建议直接换成强一致组件。

5.3 ZooKeeper方案为什么能避免脑裂

ZooKeeper方案能保证互斥性的底气来自ZAB协议。它的选举和写入流程确保:同一时刻只有一个Leader处理写请求,写请求需要在集群中达成多数派确认后才算成功。所以即使主节点挂了,新选举出的Leader也会带着完整的数据继续服务,不会出现Redis主从异步复制导致“新主节点丢失旧锁”的问题。

加上前文提到的“临时顺序节点 + Watch”机制,ZooKeeper锁可以做到:

  • 所有客户端公平排队,不存在“后来的抢先”情况。
  • 客户端会话断开锁自动释放,无需设置业务执行时间预估。
  • 锁的持有状态可以被集群中的多个节点共同确认,不存在单点时钟问题。

代价是性能。我们压测过,ZooKeeper锁的平均获取耗时在10ms量级,Redis锁在1ms以下。这意味着ZooKeeper适合低频但高一致性的场景。如果业务允许“宁可慢一点也要绝对安全”,ZooKeeper是比Redis更让你睡得安稳的方案。

5.4 等价的etcd方案

如果你不想养ZooKeeper,etcd是另一个强一致选择。etcd基于Raft协议,思路和ZooKeeper类似,而且有租约(Lease)机制可以实现自动续期,还支持通过Watch感知锁释放。社区里也有封装好的SDK。我个人的感觉是:etcd的运维体验比ZooKeeper好不少,文档也更清晰,新项目如果必须上强一致锁,可以优先考虑etcd。

6. 实战中踩过的坑:锁误删、线程飘高与超时误判

最后这部分,是我把分布式锁服务跑了一年之后沉淀下来的几个真实坑,每个都是在网上看教程学不到的。

6.1 锁误删:第一次线上并发事故的主角

前面讲了锁误删的解决方案(value带唯一标识 + Lua脚本),这里讲一个真实案例加深理解。我们的一个支付回调服务,某天突然出现两个线程同时处理同一笔支付单的状态更新。排查日志发现,线程A拿了锁,但它在调用远端接口时超时,等了40多秒才返回,而锁的过期时间只有15秒。锁过期后线程B拿到锁开始处理,此时线程A返回并执行了DEL lock_key,一瞬间B的锁没了,线程C又进来了。

三个线程同时处理同一笔单据,结果就是状态被反复覆盖、对账异常。事后修复除了加value校验和Lua脚本外,还加了一句非常重要的设计:锁的过期时间一定要用看门狗自动续期,而不是拍脑袋设一个固定值。如果当初用了Redisson看门狗,这个事故根本不会发生。

6.2 看门狗线程导致的CPU飘高

Redisson的看门狗好归好,但有一个隐藏的坑:当你业务中大量使用getLock().lock()并忘记释放时,看门狗线程会无限续期,同时锁持有时间被无限拉长。我们有一个业务方,锁释放逻辑写在了一个异常分支里漏掉的地方,导致一批锁持有了一整天,最终该业务实例的CPU和内存持续走高,直到我们把所有锁全部清理才恢复。

所以我把“锁释放”提升到了类似于“关闭IO流”的级别:能放到finally里的必须在finally里释放,绝对不能依赖任何业务正常路径。并且在SDK层加了最长持有时间兜底——即使看门狗一直在续期,超过一个上限(比如5分钟)后强制终止续期并记录告警。

6.3 GC停顿导致锁超时误判

这是Java体系下比较隐蔽的一个问题。假设你拿到了锁,执行业务过程中发生了一次Full GC,STW时间超过了锁的过期时间,那么锁就在你“不知情”的情况下失效了,别的线程开始蚕食你的临界区。等你GC结束继续执行,你和你同事的代码已经开始并发跑了。

更麻烦的是,GC停顿不是你能通过调节锁参数规避的——无论锁设置多长,理论上都可能发生一次超过它的STW。所以我的经验是:锁的过期时间设置,绝不能卡在业务执行耗时的边际上,一定要留出超过系统最坏停顿时间的余量。比如系统最差可能有1秒的Full GC停顿,那锁至少也要设2秒以上。同时,业务逻辑里尽量降低分配率,减少长停顿GC发生的概率,这是在根上优化。

6.4 锁粒度太粗导致性能瓶颈

我们曾经把“用户账户操作”锁做成了account:lock:{userId},结果发现同一个用户的高并发操作(比如同时下单和查余额)全被堵到了一把锁上。后来改成拆成两把:account:update:{userId}和account:read:{userId},读写互不阻塞,只有写写互相排斥。就这么一个改动,接口的P99从180ms降到了65ms。

这里给你一个通用的锁粒度拆分思路:

  • 按操作类型拆分:读锁和写锁分开,或者对不同操作定义不同锁key。
  • 按资源维度拆分:比如同一个用户的不同资源类型,不要共用一把锁。
  • 按关键路径拆分:只在真正需要串行化的代码块上加锁,不要把整个方法都锁起来。

6.5 试锁返回值一定要判空

用Redisson之类的SDK时,很多人会忽略tryLock的三返回值状态。部分实现中,如果设置了waitTime=0并且锁被占用,返回的是null而不是false,直接拿空对象调用后续逻辑会触发NPE。我在代码审查时经常能看到这种写法:

if (lock.tryLock(0, 30, TimeUnit.SECONDS)) { ... }

正确的是:

boolean locked = lock.tryLock(0, 30, TimeUnit.SECONDS); if (locked) { try { // ... } finally { lock.unlock(); } }

看上去是小事,但在线上直接就是一大波告警。任何锁框架的返回值,都应该先想一想“失败时它返回什么”,再决定判断逻辑。

6.6 别忘了业务本身要有兜底

最后一条也是我认为最重要的一条:分布式锁永远无法百分之百保证互斥性,所以业务层必须有自己的幂等校验和兜底机制。我参与的每一个核心链路,都不会把分布式锁当作唯一防线。通常的组合是:分布式锁做第一道并发控制关卡,数据库唯一索引/乐观锁做第二道兜底,最终的效果校验再确认一次结果。

如果你试着把锁当作唯一防线,你会陷入一个死循环:为了防丢锁升级成RedLock,RedLock也有理论漏洞,继续找更强的方案……实际上没有一个分布式锁方案能在所有极端情况下都保证安全。成熟的架构师会让锁承担99.9%的场景,剩下的0.1%交给业务兜底。

做了这么久分布式系统,我最大的体会是:分布式锁不是银弹,能用队列削峰的地方优先用队列,能用事务解决的就别上锁,真正需要锁的场景,要想清楚最坏情况,并让系统即使在最坏情况下也能自愈。这也正是我把锁从工具类做成服务、加上监控和治理的初心——不追求一把绝不失效的锁,而是让锁失效时系统依然稳定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询