我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Redis分布式锁全解析:从原理到高并发实战避坑

Redis分布式锁全解析:从原理到高并发实战避坑 1. 为什么单机锁到了分布式环境就失灵了先从一个最日常的场景说起。你在写一个订单服务扣减库存用的是 synchronized 或者 ReentrantLock在单机部署的时候一切正常请求再多也不会超卖。后来系统要支撑更高的并发你把服务横向扩容到了三台机器Nginx 做负载均衡结果线上开始出现库存扣成负数的情况。问题几乎可以确定出在锁上——三台机器各自持有一把 JVM 内部的锁互相之间完全隔离A 机器锁住的代码段B 机器和 C 机器照样可以同时进去执行。这就是分布式锁要解决的第一个本质问题在多个进程、多台机器之间实现临界区资源的互斥访问。单机锁靠的是 JVM 内存中的监视器Monitor分布式环境下根本没有共享内存必须引入一个所有进程都能访问到的第三方来协调。这个第三方可以是 Redis、ZooKeeper、etcd、数据库核心职责都一样——提供一个全局唯一的、所有节点都能达成一致的令牌。我在实际项目里见到过不少类似的错误设计最常见的是用数据库唯一索引当锁。比如建一张锁表插入一条记录表示拿到锁释放锁就删掉记录。这个方案在小规模场景下能用但性能天花板很低每次加锁解锁都是一次磁盘 IO而且还要自己处理连接池、超时、宕机恢复等一系列问题。真正在生产环境里用得最多的还是 Redis原因很直接性能好、实现简单、生态成熟。后面的内容我会以 Redis 为主线展开同时把 ZooKeeper 和 etcd 的对比放在后面讲方便你在做技术选型的时候心里有数。这篇文章不打算只讲理论。我会先带你从零实现一个可用的 Redis 分布式锁再逐步加细节处理各种边界情况最后聊一聊 Redlock 的争议、主从切换带来的坑以及面试官最爱问的那几个问题。不管是刚接触分布式锁的初学者还是准备面试的候选人这篇文章都能给你一套完整的、可以直接落地的知识框架。2. Redis 分布式锁的核心实现2.1 从 SETNX 到 SET NX EX一条命令解决原子性问题Redis 分布式锁最基础的版本大家可能都听过 SETNX 命令。SETNX 是 SET if Not eXists 的缩写只有当 key 不存在的时候才能设置成功正好可以用来模拟抢占的行为。最早的实现方式是这样的伪代码// 加锁 Long result jedis.setnx(lock:order:123, 1); if (result 1) { // 拿到锁执行业务逻辑 } else { // 没拿到锁重试或返回失败 } // 释放锁 jedis.del(lock:order:123);这个版本有一个致命问题如果拿到锁的线程在执行过程中宕机了或者代码抛异常没有走到 del 那一行这个 key 就永远留在 Redis 里了其他线程再也拿不到锁。这叫做死锁在实际生产环境里一旦发生就是事故。解决办法是给锁加过期时间让 Redis 在 key 过期后自动删除。于是代码变成了这样Long result jedis.setnx(lock:order:123, 1); if (result 1) { jedis.expire(lock:order:123, 10); // 执行业务逻辑 }但是这里又引入了一个新的原子性问题setnx 和 expire 是两条独立的命令如果 setnx 执行成功之后、expire 执行之前进程崩溃了key 依然没有过期时间死锁问题照样存在。这个问题的根源在于多步操作不具备原子性在并发场景下任何两步之间都可能被意外打断。正确的做法是使用 Redis 2.6.12 版本之后提供的 SET 命令扩展参数把加锁和设置过期时间合成一条命令String result jedis.set(lock:order:123, 1, NX, EX, 10); if (OK.equals(result)) { // 拿到锁 }SET key value NX EX seconds 的含义是当 key 不存在时设置成功同时设置过期时间为 seconds 秒。这两件事在 Redis 内部是原子完成的从根本上杜绝了设置锁成功但没设置过期时间的窗口期。这是 Redis 分布式锁的第一个核心要点也是面试中最高频的考点之一必须把为什么要用一条命令的原因讲清楚。2.2 释放锁为什么要用 Lua 脚本锁加上了过期时间也设置好了但释放锁这一步同样有坑。先看下面这段代码// 业务逻辑执行完毕 jedis.del(lock:order:123);问题在于线程 A 拿到锁后执行了较长时间锁已经因为超时被 Redis 自动释放了。这时候线程 B 拿到了锁开始执行业务逻辑。突然线程 A 执行完毕执行 del 命令把锁删掉了。线程 B 的锁被误删紧接着线程 C 也能拿到锁临界区同时进入了两个线程互斥彻底失效。解决办法是释放锁时校验 value 是否为当前线程持有锁时写入的标识。这个标识可以用 UUID 生成加锁时作为 value 写入释放锁时先 GET 判断是不是自己的值是才能删。于是释放锁的伪代码变成了String token UUID.randomUUID().toString(); jedis.set(lock:order:123, token, NX, EX, 10); // 业务逻辑... // 释放锁 if (token.equals(jedis.get(lock:order:123))) { jedis.del(lock:order:123); }如果学过并发编程你会发现这里犯了和前面类似的错误——GET 和 DEL 是两步操作中间存在时间窗。线程 A 判断 value 是自己的还没执行 DEL锁恰好过期了线程 B 立刻加锁成功。此时线程 A 执行 DEL还是会把 B 的锁删掉。所以判断和删除之间依然需要原子性。Redis 官方给出的标准答案是使用 Lua 脚本把比较 value 删除 key放进一个脚本里Redis 会保证脚本整体原子执行String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; jedis.eval(script, Collections.singletonList(lock:order:123), Collections.singletonList(token));这段脚本的逻辑很简单先获取锁对应的 value如果等于当前线程的 token就删除 key返回 1否则返回 0。因为 Redis 是单线程执行命令的eval 执行期间不会有其他命令插入所以这个比较和删除是不可分割的。到这里一个可用的 Redis 分布式锁已经成型加锁用 SET NX EX 一条命令释放锁用 Lua 脚本保证原子性value 用 UUID 标识持有者身份防止误删。这是 Redis 分布式锁的基础版本也是很多开源框架比如 Redisson的底层雏形。我建议你在写简历项目的时候至少要把这个演进过程想明白从 SETNX 到 SET NX EX、从直接 DEL 到 Lua 脚本每一步解决什么问题都要能讲清楚。2.3 一个可以抄作业的 Java 实现光讲理论不够我写一个完整的 Java 实现基于 Jedis可以直接拿来跑实验。生产环境建议用 Redisson 或者 Lettuce但理解原理用 Jedis 最直观。public class RedisDistributedLock { private static final String LOCK_SUCCESS OK; private static final Long RELEASE_SUCCESS 1L; private static final String SET_IF_NOT_EXIST NX; private static final String SET_WITH_EXPIRE_TIME EX; private static final String RELEASE_LOCK_SCRIPT if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; private Jedis jedis; private String lockKey; private String lockValue; private int expireTime; public RedisDistributedLock(Jedis jedis, String lockKey, int expireTime) { this.jedis jedis; this.lockKey lockKey; this.expireTime expireTime; this.lockValue UUID.randomUUID().toString(); } /** * 加锁获取不到直接返回 false */ public boolean tryLock() { String result jedis.set(lockKey, lockValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); return LOCK_SUCCESS.equals(result); } /** * 加锁带重试机制在指定时间内不断尝试 */ public boolean tryLock(long waitTime) throws InterruptedException { long endTime System.currentTimeMillis() waitTime; while (System.currentTimeMillis() endTime) { if (tryLock()) { return true; } Thread.sleep(50); } return false; } /** * 释放锁使用 Lua 脚本保证原子性 */ public boolean unlock() { Object result jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(lockValue)); return RELEASE_SUCCESS.equals(result); } }使用的时候要注意几点。第一expireTime 的设置非常讲究不能太短也不能太长。太短可能导致业务还没执行完锁就过期了另一线程进来并发执行太长的话如果持有锁的线程宕机了其他线程要等很久才能拿到锁。一个相对通用的做法是根据业务的最大执行耗时来估算再留 2 到 3 倍的余量。第二tryLock 的重试间隔不要设置成固定值可以用随机值避免多个线程同时重试造成羊群效应。第三业务代码里要在 finally 块中释放锁确保任何异常路径下锁都能被正确释放否则异常线程会一直持有锁直到过期。还需要强调一点这个实现里的 expireTime 是死值锁不会自动续期。如果业务执行时间不可控建议后续接入看门狗机制自动续期这个在 Redisson 里已经实现了后面会详细说。3. 分布式锁的四个经典翻车现场3.1 锁过期引发的羊群效应和并发穿透锁过期是分布式锁最让人头疼的问题。线程 A 拿到锁正常情况应该 200 毫秒执行完但你设的过期时间是 1 秒。结果那一次业务特别慢GC 停顿加上下游接口超时整个花费了 1.2 秒。此时锁已经过期被 Redis 删除线程 B 拿到锁也进来执行了两个线程同时操作同一份资源互斥被破坏。这个问题的根因是持有锁的时间超过了预设的过期时间而我们的实现无法感知这一点。有的同学说我把过期时间设大一点不就行了吗设大只能降低概率不能根除。你永远无法准确预测业务在最坏情况下的执行时长。业界对这个问题的解法是看门狗机制也就是自动续期。Redisson 的实现方式是加锁成功后会启动一个后台定时任务默认每 10 秒执行一次如果锁还被当前线程持有就把锁的过期时间重置为 30 秒。这样业务执行多久锁就能续多久只有等到业务主动释放或者线程宕机锁才会真正失效。如果你是自己实现也可以做一个简化版的续期逻辑。加锁后启动一个定时任务每隔 expireTime / 3 秒检查一次锁是否还是自己的如果是就重新执行 expire 命令业务结束后取消定时任务。这是一个加分项面试官听到这里通常会眼前一亮。3.2 主从切换导致锁丢失为什么 RedLock 存在争议Redis 的高可用架构通常是主从复制加哨兵。客户端向主节点写入锁主节点异步复制到从节点。问题就在于异步两个字如果客户端刚在主节点写入锁主节点还没把数据复制给从节点就宕机了哨兵会选择一个从节点提升为新的主节点。新的主节点上没有这条锁记录其他客户端就可以成功加锁原来那个持有锁的客户端在逻辑上就失效了。这是一个非常经典的分布式系统问题Redis 的主从复制是异步的所以无法保证锁的持久性。对于大多数业务场景这个风险可以接受因为锁丢失的前提是主节点恰好在这个极短的窗口内宕机。但在金融支付等对一致性要求极高的场景这就是不可接受的。针对这个问题Redis 的作者 Antirez 提出了 RedLock 算法。核心思想是部署 N 个互相独立的 Redis 节点通常 N5客户端需要向所有节点发起加锁请求只有成功锁住超过半数的节点N/21才算真正拿到锁。释放锁时向所有节点发送释放请求。这样即使少数节点宕机只要大多数节点还保留着锁记录整个锁依然有效。RedLock 在理论界引发了大量讨论尤其是分布式系统领域的权威专家 Martin Kleppmann《Designing Data-Intensive Applications》的作者专门写过一篇长文批评 RedLock认为它存在不少逻辑漏洞。主要争议点包括客户端加锁耗时过长导致锁在不同节点上的有效时间不一致、GC 停顿会让锁在客户端不知情的情况下过期、时钟跳跃会导致过期时间计算失准等。这些问题的本质是RedLock 仍然依赖时间和时钟而分布式系统里时间本身就是一个不可靠的变量。我和大多数同行的态度是RedLock 有理论瑕疵但不是不能用的方案。如果你的系统既需要 Redis 的性能又对锁的可靠性有较高要求可以用 RedLock但心里要清楚它并不能做到 100% 安全。如果需求是绝对可靠更好的选择是 ZooKeeper 或者 etcd它们用临时顺序节点 会话或者租约 版本号的机制从底层设计上规避了时间依赖问题。这个对比我在后面第 5 节详细展开。3.3 可重入性同一个线程重复加锁怎么办先想一个问题如果你的业务代码里方法 A 加锁后调用了方法 B方法 B 内部也要加同一把锁你的线程会被自己挡住吗在单机 ReentrantLock 里当然不会因为 ReentrantLock 支持可重入内部用 holdCount 记录当前线程持有锁的次数。但上面我们实现的 Redis 分布式锁压根不认识线程同一个线程第二次执行 SET NX EX 时发现 key 已经存在就会返回失败。如果策略是失败就重试那就死锁了——线程自己等自己释放锁。解决思路是在锁的 value 里记录持有者的标识和重入次数。Redis 官方推荐的方式是把 value 设置为一个 Hashfield 存持有者标识field 对应的值存重入计数。Redisson 就是这么做的它内部用一个 Hash 结构存储锁信息加锁时记录线程 ID 和计数解锁时计数减一减到零才真正删除 key。虽然 Redisson 的底层实现比较复杂但使用它的 API 时你感知不到这层复杂性它就是天然支持可重入的。如果你是自己写底层实现可重入逻辑可以做得简单一点加锁时如果发现 key 已存在就 GET 一下 value如果是当前线程的标识就允许进入并把这个线程的重入次数加一。释放锁时重入次数减一减到零才执行 DEL。这里有一个工程上的小陷阱可重入计数存在 Redis 里如果锁过期了计数也就没了。这种情况下即使线程还在执行它后续的重入加锁也会失败。所以做可重入锁时一定要配合看门狗续期让锁的生命周期覆盖整个执行过程。3.4 锁粒度与热点 key别把整条街的锁挂在同一根柱子上还有一个容易被忽略但非常重要的问题——锁的粒度。很多初学者喜欢用一个全局锁 key比如 lock:inventory所有库存操作都靠这一把锁串行执行。这样确实简单但并发能力被压到极低所有请求都排队等着同一把锁。正确做法是把锁的粒度细化为业务维度比如 lock:inventory:sku_123456每个商品一个锁不同商品的锁互不干扰。再进一步如果某个商品被秒杀单把锁依然是热点这时候就需要用分段锁的思路。把库存拆成多个段比如 100 个库存拆成 10 段每段 10 个库存每段一把锁用户请求到达时先对用户 ID 做哈希取模路由到固定的段只在段内竞争锁。这是典型的分而治之思想在秒杀系统里很常见。我在一个真实项目里见过一个极端的例子所有订单操作都加同一把锁压测时 QPS 一到 200 就开始大量超时后来把锁 key 从 lock:order 改成 lock:order:{orderId}QPS 直接到 2000 以上都没压力。锁的粒度对性能的影响就是这么显著。不过要提醒一句锁粒度越细实现复杂度越高。分段锁要考虑数据一致性不同段之间的数据如果有关联操作需要仔细设计路由规则不能让一个事务跨多个段。高并发场景下简单的全局锁往往是最不容易出错的性能瓶颈可以通过其他方式优化比如异步化、削峰填谷。4. 生产级方案Redisson 与看门狗机制4.1 为什么建议直接用 Redisson看到这里你应该已经意识到从零写一个生产可用的分布式锁需要考虑的问题非常多原子性、过期时间、可重入、自动续期、主从切换、重试策略……每一个都暗藏深坑。除非你有特殊需求否则我的建议是直接用 Redisson它是目前 Java 生态里最成熟的 Redis 分布式锁实现。Redisson 是 Redis 官方推荐的 Java 客户端框架它对分布式锁的支持非常完整。你只需要引入依赖写几行代码就能用上功能完善的分布式锁Config config new Config(); config.useSingleServer().setAddress(redis://127.0.0.1:6379); RedissonClient redisson Redisson.create(config); RLock lock redisson.getLock(lock:order:123); try { // 尝试加锁最多等待 10 秒加锁成功后 30 秒自动过期 if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }这段代码背后Redisson 替我们做了哪些事情我列一下关键点方便你在面试的时候展开讲。第一可重入。Redisson 的 RLock 支持可重入底层是用 Redis Hash 结构实现计数同一个线程可以多次加锁多次解锁后才真正释放锁。第二自动续期。默认情况下加锁成功后锁的有效期是 30 秒。如果业务逻辑在锁的有效期内还没执行完Redisson 的后台线程会每 10 秒检查一次锁的状态只要锁还在且由当前线程持有就自动把过期时间重置为 30 秒。这就是所谓的看门狗机制解决了锁过期但业务未完成的问题。第三等待锁的机制。tryLock 支持传入 waitTime获取不到锁会阻塞等待而不是立即返回失败。超时后会返回 false你需要根据返回值决定是否走降级逻辑。第四公平锁和多锁支持。Redisson 还提供了 FairLock公平锁和 MultiLock多节点锁可以应对一些特殊场景。公平锁内部基于 Redis 的 List 和 Hash 结构实现了一个等待队列保证先到的线程先获取锁适合对公平性有要求的场景。4.2 看门狗的参数真相30 秒和 10 秒是怎么来的Redisson 的看门狗机制网上讲得很多但不少人知其然不知其所以然。默认配置的 lockWatchdogTimeout 是 30000 毫秒也就是 30 秒。后台续期的定时任务执行时间是 lockWatchdogTimeout / 3也就是 10 秒。为什么是 30 秒和 10 秒这个比例不是凭空设计的它的逻辑是这样的续期任务必须在锁真正过期前执行否则锁过期了再去续期没有意义。如果每隔 10 秒执行一次续期而锁的有效期是 30 秒那么即使某一次续期因为网络抖动失败了还有机会在下一次续期时补救因为整个过期周期内最多可能丢失两次续期机会。网络抖动在分布式环境下是常态这个设计相当于给锁的有效期做了一个兜底让它能容忍短时间的网络瞬断。你可能会问为什么不定时 1 秒续一次那样更安全。这是性能与安全性的权衡。续期操作本身就是一次 Redis 请求每秒执行一次会极大地增加 Redis 的压力。每 10 秒一次在保证安全性的前提下把 Redis 请求量降到了相对合理的水平。还有一个常见的坑如果你手动调用 lock.tryLock(waitTime, leaseTime, unit)传入了 leaseTime锁的持有时间Redisson 就不会启动看门狗。因为你自己已经明确了锁的有效期框架会认为你不需要自动续期。如果你想用看门狗就不要传 leaseTime或者传 -1。这个细节在官方文档里有说明但很容易被忽略。我见过有同事在调用 tryLock 时传了 leaseTime业务执行超过该时间后锁被强制释放出现并发问题排查了半天才发现是这里的问题。4.3 集群模式下 Redisson 的锁有什么不同Redisson 支持多种 Redis 部署模式包括单节点、主从、哨兵和集群。需要注意的是在不同模式下锁的表现不太一样。单节点模式下锁的可靠性完全依赖于这一个节点节点宕机锁就丢了应用层可能完全感知不到。主从模式下锁写入主节点后异步复制如果主节点故障且数据未同步锁会丢失这是我们前面提到的经典问题。哨兵模式只是增加了故障自动切换能力不能解决复制延迟带来的锁丢失问题。Redisson 针对集群模式提供了一种红锁的实现也就是 MultiLock。你可以在多个独立的 Redis 节点上加同一把锁只有当大多数节点都成功加锁时才返回成功。但说实话在真正的 Redis Cluster 模式数据分片模式下Redisson 加锁时锁 key 会被哈希到固定的槽位锁本身只存在于某一个分片节点上。Cluster 的分片机制并不为锁提供跨节点容错。所以如果你真的需要高可靠的分布式锁不要指望 Redis Cluster 特性本身能帮你。要么接受单点风险要么手动在多个独立 Redis 实例上部署 Redisson MultiLock要么换 ZooKeeper/etcd。我在企业里看到的普遍做法是业务不涉及资金用 Redis 单主从加哨兵接受极低概率的锁丢失涉及资金对账等敏感操作直接用 ZooKeeper 或者数据库事务保证强一致。5. 进阶对比ZooKeeper 与 etcd 的分布式锁5.1 ZooKeeper 临时顺序节点是怎么实现锁的Redis 分布式锁的性能和简单性是优势但它基于过期时间做兜底本质上是一个尽力而为的方案。如果你的业务对强一致有硬性要求目前业界最可靠的方案是 ZooKeeper 或 etcd。ZooKeeper 实现分布式锁的原理核心是两类节点临时节点和顺序节点。临时节点在客户端会话结束时自动删除即使客户端宕机也不会出现死锁。顺序节点则自动获得一个全局递增的序号客户端可以通过序号判断自己在等待队列中的位置。加锁的流程是这样的客户端在指定锁路径下创建一个临时顺序节点比如 /locks/lock_0000000001。然后获取该路径下的所有子节点对节点序号排序。如果自己创建的节点序号最小说明自己获得了锁如果不是最小的就监听序号比自己小的前一个节点等待它被删除。释放锁的流程更简单客户端主动删除自己创建的节点或者会话超时由 ZooKeeper 自动删除。一旦锁节点被删除监听它的下一个客户端就会被唤醒重新检查自己是否是最小序号节点。这个方案有两个明显优点。第一不需要设置过期时间极端情况下也不会出现锁长期占用的问题因为会话超时兜底。第二加锁和解锁是严格按照顺序进行的先来后到不存在 Redis 那种锁过期了另一线程突然插队的情况。缺点是性能不如 RedisZooKeeper 的写操作要走 ZAB 协议延迟和吞吐量都不在一个量级。另外 ZooKeeper 的会话超时机制会导致一种特殊问题客户端没有宕机但因为 GC 停顿或网络分区导致心跳丢失ZooKeeper 认为会话失效删除临时节点其他客户端就能拿到锁而原客户端还在继续执行。5.2 etcd 的租约和版本号机制etcd 是 Kubernetes 生态中的核心组件实现分布式锁用到了两个关键特性租约Lease和版本号Revision。比 ZooKeeper 更现代的一点是etcd 提供了 TTL 机制客户端可以创建租约并持续续约这比 ZooKeeper 的心跳机制更可控。etcd 加锁的原理和 ZooKeeper 类似也是在同一个前缀下创建 key每个 key 会有一个全局递增的 Revision。客户端创建自己的 key 后查询前缀下的所有 key看看自己的 Revision 是否最小。如果最小就获得锁否则监听比自己小一个 Revision 的 key 的删除事件。etcd 的租约机制解决了两个问题。第一客户端崩溃后锁能自动释放因为租约到期后关联的 key 会自动删除。第二客户端可以主动续约只要业务没有结束锁就不会被释放避免了 Redis 那种锁过期但业务还在执行的问题。相比 ZooKeeperetcd 在运维便捷性和社区活跃度上有优势而且和云原生生态的结合更紧密。如果你的系统已经在 Kubernetes 上运行直接使用 etcd 做分布式锁几乎是零额外成本的。但如果你只是在一个传统的 Java 服务里需要一个分布式锁引入一套新的 etcd 集群作为协调者通常有点重Redis 往往够用了。5.3 三种方案横向对比与选型建议我把 Redis、ZooKeeper、etcd 三种方案放在一张表里对比方便你对照选择。维度RedisZooKeeperetcd性能高微秒级延迟中毫秒级延迟ZAB 协议写开销大中Raft 协议写开销大可靠性依赖过期时间和主从复制极端情况可能丢锁临时节点 会话机制可靠性高租约 Revision可靠性高实现复杂度低SET NX 即可框架支持完善中需要理解节点类型和 Watcher中需要理解租约和 Revision自动续期需要自己实现或使用 Redisson 看门狗会话本身就包含心跳无需额外处理租约自动续期机制公平性无抢占式有按顺序节点排序有按 Revision 排序典型场景高并发缓存场景、秒杀、幂等控制配置推送、分布式协调、强一致场景云原生环境、Kubernetes 生态选型没有绝对的好坏只有合不合适。我给你的建议是如果你的应用是互联网业务、QPS 高、对锁的要求是基本可靠直接用 Redis 加 Redisson这是性价比最高的方案。如果你的应用涉及资金、对账、库存等强一致场景且 QPS 不高用 ZooKeeper 或者 etcd 更稳。如果你想在云原生架构里做协调服务etcd 是最自然的选择。6. 分布式锁面试题高频考点拆解6.1 面试官最爱问的 7 个问题分布式锁是 Java 后端面试的高频考点几乎每一面都会遇到。我整理了 7 个出现频率最高的问题每个都附带答题思路你可以照着梳理自己的回答逻辑。第一个问题你们项目里为什么用分布式锁不用它行不行回答时要说清楚背景比如服务多实例部署后单机锁失效资源并发访问出现数据错乱。如果再延伸一点可以说明为什么不能用 synchronized 或者数据库乐观锁替代。这个问题的核心是考察你有没有真实的高并发场景经验。第二个问题Redis 分布式锁怎么实现从 SET NX EX 命令说起解释原子性然后补充 Lua 脚本释放锁最后提 value 用 UUID 防误删。如果能画出完整的时序图加分。第三个问题锁过期了业务没执行完怎么办标准答案是看门狗自动续期讲 Redisson 的实现细节包括 30 秒过期、10 秒续期、续期失败的处理。第四个问题主从切换会导致锁丢失怎么解决先说清问题本质是异步复制然后提 RedLock 的多数派思想再说 RedLock 的争议点最后指出更可靠的替代方案是 ZooKeeper 或者 etcd。第五个问题分布式锁如何保证可重入说明用 Hash 结构记录线程标识和重入计数来源可以提 Redisson 的实现。第六个问题锁的粒度怎么设计举一个全局锁和分段锁对比的例子说明锁粒度对性能的影响然后延伸到热点 key 场景。第七个问题ZooKeeper 实现分布式锁和 Redis 有什么区别从三个维度回答实现原理、可靠性、性能。如果能讲清楚 ZooKeeper 临时顺序节点的原理并且对比 Redis 的过期时间机制面试官对你的评价会明显上一个档次。6.2 从会背题到会讲题的三层递进我发现很多候选人对分布式锁的知识有一个共同问题能说出 SET NX EX、Lua 脚本、RedLock 这些名词但一旦被追问细节就露馅。比如你问为什么释放锁要用 Lua 脚本他会背答案是为了保证原子性但你继续问为什么 GET 和 DEL 两条命令不能保证原子性他就愣住了。面试官真正想考察的其实不是你是否知道答案而是你有没有真正理解背后的逻辑。建议你按照场景 - 问题 - 方案 - 原理这个路径组织回答不要一上来就甩名词。比如问你 Redis 分布式锁的实现不要只说命令要先把场景还原多个服务实例并发操作同一资源需要互斥单机锁做不到跨进程互斥Redis 作为共享存储可以充当协调者进一步思考宕机怎么办所以加过期时间再进一步想释放时判断和删除要原子所以用 Lua 脚本。这样层层递进地回答面试官会知道你是在真正思考而不是背诵答案。还有一个很容易被忽略的加分项主动讲出方案的局限性和适用场景。比如你答完 Redis 锁的实现后自己主动说但是这个方案在极端场景下会丢锁所以如果对一致性要求极高我会考虑 ZooKeeper这会给面试官留下一个候选人有大局观、真正理解技术决策背后的 trade-off的印象。6.3 一个完整的回答模板最后我给你一个可以直接套用的回答模板以Redis 分布式锁怎么实现为例我们项目里使用 Redisson 框架实现分布式锁。核心原理是使用 Redis 的 SET NX EX 命令保证加锁和设置过期时间的原子性。锁的 value 存放当前请求的唯一标识防止误删。释放锁时使用 Lua 脚本先校验 value 再删除 key整体原子执行。Redisson 在底层用 Hash 结构支持可重入同时通过看门狗机制自动续期解决业务执行时间超过锁过期时间的问题。这个方案的优点是性能好、接入简单缺点是 Redis 主从复制是异步的极端场景下主节点宕机可能导致锁丢失所以如果业务对一致性要求极高我会选择 ZooKeeper 或 etcd 实现。这段回答覆盖了命令细节、原子性、防误删、可重入、看门狗、可靠性边界和替代方案层次清晰信息密度高足够支撑起一次深度的面试追问。如果你能把这里的每一句话都展开讲清楚分布式锁这道题基本就过关了。7. 分布式锁实战经验与避坑建议最后分享一些我在真实项目中积累的经验有些是用线上故障换来的教训希望能帮你少走弯路。第一业务代码里必须用 finally 释放锁。这一点看起来是老生常谈但我知道至少有三分之一的人犯过这个错误。一旦业务逻辑抛出异常锁没有释放其他线程只能等着锁过期。如果过期时间设置得比较长比如 30 秒而每次请求都会触发异常那这台服务的错误请求就像滚雪球一样把并发能力耗尽。在写分布式锁代码的时候请把 try-finally 当成一种肌肉记忆。第二锁的过期时间不能拍脑袋定。我见过不少项目把过期时间统一设成 10 秒理由是够用了。但实际上不同的业务操作耗时差异很大一个简单的缓存更新可能 50 毫秒就结束一个涉及外部接口调用的业务可能要好几秒。正确的做法是针对每个业务场景评估最坏执行时间然后在这个基础上乘一个安全系数。如果业务耗时无法预估就一定要用看门狗自动续期不要再依赖固定过期时间。第三监控锁的获取耗时和等待耗时。分布式锁是一个典型的隐性依赖出了问题不会第一时间暴露而是表现为接口变慢、超时率上升。建议对所有加锁操作做埋点统计加锁等待时间、持锁时间两个指标。一旦持锁时间接近过期时间要立刻报警。我之前的团队就靠这个监控在锁的过期时间设置不合理时提前发现避免了上线后的事故。第四不要在持锁期间做耗时操作。锁保护的应该是操作共享资源的临界区而不是整个业务流程。很多人习惯把加锁写在接口入口处然后拿着锁去查数据库、调外部接口、做复杂计算这些操作动辄几百毫秒甚至几秒。这把锁的粒度放大了十倍不止严重拖垮了系统的并发能力。正确的做法是先在锁外面做好所有能提前做的准备只把真正需要互斥的那一小段代码放进锁里。第五加锁失败后的降级策略要想清楚。拿到锁执行正常逻辑拿不到锁怎么办有的系统是直接返回失败有的系统是重试等待有的系统是走降级路径。不同的策略对用户体验和系统负载的影响完全不一样。如果你的锁冲突率较高建议用等待重试 超时降级的组合策略不要让所有请求同时打向 Redis。分布式锁本质上不是银弹。它能解决跨进程互斥的问题但无法自动解决业务逻辑设计上的缺陷。如果你发现系统频繁出现锁冲突、锁等待超时先把业务代码里锁保护的代码段反复看几遍也许更好的解法是把锁去掉调整数据结构或者业务逻辑而不是换一种更复杂的锁实现。我在实际项目里体会最深的一件事是分布式锁的方案演进本质上是业务需求和技术风险之间的博弈。你妥协了某一部分可靠性就要在其他地方设计补偿机制。比如用 Redis 锁丢了一个订单的幂等控制就要靠数据库的唯一索引兜底。以我个人的经验把分布式锁的底层原理吃透再结合实际业务做权衡远比背下来的 command 和代码更有价值。希望这份拆解能帮你在自己的项目里正确、安全地用上它。
返回列表