【大白话说Java面试题 第171题】【07_Redis篇】第7题:Redis 实现分布式锁的原理 PDF大白话说Java面试题 — 07_Redis篇第7题Redis 实现分布式锁的原理回答核心考点Redis 分布式锁是分布式系统中协调多节点对共享资源互斥访问的核心机制。大厂面试不会只问SETNX Lua 脚本这种基础实现而是深入考察SET 命令原子参数NX/EX/PX的语义差异、看门狗自动续期机制、Redlock 算法的争议与缺陷Martin Kleppmann 论文以及Redisson 的底层实现Netty 连接池、Pub/Sub 解锁通知、锁续期 Watchdog。面试官真正想判断的是你是否理解分布式锁从能用到生产级可靠的完整演进路径以及每种方案的适用边界和坑点。1. 基础实现SETNX EXPIRE 的演进1.1 早期实现有缺陷# 步骤1加锁SETNX lock:order:10011# 返回1表示获取成功EXPIRE lock:order:100130# 设置30秒过期时间# 步骤2业务逻辑...# 步骤3释放锁DEL lock:order:1001致命缺陷SETNX和EXPIRE是两个独立命令非原子操作。如果执行SETNX后 Redis 宕机或客户端崩溃EXPIRE未执行锁将永久存在死锁。1.2 改进实现SET 原子命令Redis 2.6.12# 原子加锁NXNot Exists EX秒级过期SET lock:order:1001 unique_value NX EX30参数语义说明NXOnly set if Not eXistsKey 不存在时才设置实现互斥XXOnly set if eXistsKey 存在时才设置用于更新EX seconds秒级过期时间EX 30表示 30 秒后自动释放PX milliseconds毫秒级过期时间PX 30000等同于EX 30关键改进SET命令将加锁和设置过期时间合并为原子操作彻底避免死锁。1.3 释放锁Lua 脚本保证原子性# 释放锁的 Lua 脚本原子执行 GET 判断 DELifredis.call(GET, KEYS[1])ARGV[1]thenreturnredis.call(DEL, KEYS[1])elsereturn0end为什么必须用 Lua 脚本场景非原子操作风险Lua 脚本保障GET 后判断归属再 DEL判断后、删除前锁可能已过期被其他线程获取导致误删GET判断DEL 原子执行无中间状态// Java 调用示例StringluaScriptif redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end;LongresultredisTemplate.execute(newDefaultRedisScript(luaScript,Long.class),Collections.singletonList(lock:order:1001),uniqueValue);2. 生产级问题与解决方案2.1 问题一锁不可重入问题描述同一线程在持有锁期间再次请求同一锁时会阻塞自己等待自己导致死锁。// 线程A获取锁后调用需要同一锁的方法BpublicvoidmethodA(){lock(order:1001);// 获取成功methodB();// 内部调用}publicvoidmethodB(){lock(order:1001);// ❌ 阻塞自己等待自己}解决方案可重入锁设计# Hash 结构存储field线程标识, value重入次数HSET lock:order:1001 thread_id:123451# 首次加锁value1HINCRBY lock:order:1001 thread_id:123451# 重入value2HINCRBY lock:order:1001 thread_id:12345-1# 释放value1HINCRBY lock:order:1001 thread_id:12345-1# 释放value0 → DELRedisson 实现使用 Redis Hash 结构key为锁名field为UUID:ThreadIDvalue为重入次数。2.2 问题二锁不可重试问题描述SETNX获取锁失败时直接返回业务线程需自行实现循环重试逻辑。解决方案阻塞式重试 订阅解锁通知// Redisson 的 tryLock 实现逻辑publicbooleantryLock(longwaitTime,longleaseTime,TimeUnitunit){longdeadlineSystem.currentTimeMillis()unit.toMillis(waitTime);StringthreadIdThread.currentThread().getId();// 1. 首次尝试获取锁LongttltryAcquire(leaseTime,unit,threadId);if(ttlnull)returntrue;// 获取成功// 2. 获取失败计算剩余等待时间longremainingdeadline-System.currentTimeMillis();if(remaining0)returnfalse;// 3. 订阅该锁的解锁通知频道RFutureRedissonLockEntrysubscribeFuturesubscribe(threadId);// 4. 等待解锁通知或超时subscribeFuture.await(remaining,TimeUnit.MILLISECONDS);// 5. 收到通知后再次尝试获取锁循环直到超时while(remaining0){ttltryAcquire(leaseTime,unit,threadId);if(ttlnull)returntrue;remainingdeadline-System.currentTimeMillis();}returnfalse;}核心优化通过 Redis Pub/Sub 订阅锁的解锁频道redisson_lock__channel:{lockName}避免忙等待CPU 空转实现事件驱动的高效重试。2.3 问题三超时释放业务执行时间 锁过期时间问题描述业务执行时间超过锁的过期时间锁被自动释放其他线程获取锁后并发操作数据导致数据不一致。时间线线程 A线程 B说明T1获取锁30秒过期——T2执行业务逻辑GC 或慢查询导致耗时 40 秒——T3锁自动过期释放获取锁成功线程 A 仍在执行但锁已易主T4业务完成执行 DEL—❌ 误删线程 B 的锁解决方案看门狗Watchdog自动续期// Redisson 看门狗机制默认启用RLocklockredissonClient.getLock(order:1001);lock.lock();// 不指定 leaseTime 时启用看门狗// 底层逻辑// 1. 初始过期时间30 秒lockWatchdogTimeout / 3 * 2 20 秒实际为 30 秒// 2. 看门狗线程每 10 秒leaseTime / 3检查一次// 3. 如果业务线程仍持有锁自动续期到 30 秒// 4. 业务完成或线程中断时停止续期看门狗核心参数参数默认值说明lockWatchdogTimeout30000ms锁看门狗超时时间续期周期10000mslockWatchdogTimeout / 3续期目标30000ms每次续期恢复到lockWatchdogTimeout注意如果显式指定leaseTime如lock.lock(10, TimeUnit.SECONDS)看门狗不会启用锁在 10 秒后强制释放。2.4 问题四主从一致性Redis 主从架构下的锁丢失问题描述客户端 A 在主节点获取锁成功主节点尚未将锁信息同步到从节点时宕机从节点晋升为主节点客户端 B 在新主节点获取同一锁成功客户端 A 和 B 同时持有锁互斥性被破坏。Redlock 算法Redis 作者 antirez 提出1. 获取当前时间戳 T1 2. 依次向 N 个独立的 Redis 节点非主从集群发送 SET key unique_value NX PX 30000 3. 统计获取成功的节点数且总耗时 锁过期时间 4. 如果成功节点数 N/2 1则获取锁成功 5. 锁的有效时间 过期时间 - (T2 - T1) - 时钟漂移补偿 6. 解锁时向所有节点发送 DEL 命令Redisson 实现ConfigconfignewConfig();config.useRedLock(newRedissonClientConfig(redis://192.168.0.1:6379),newRedissonClientConfig(redis://192.168.0.2:6379),newRedissonClientConfig(redis://192.168.0.3:6379));RedissonClientredissonRedisson.create(config);RLocklockredisson.getRedLock(order:1001);lock.lock();Redlock 的争议Martin Kleppmann 论文质疑点说明时钟漂移如果 Redis 节点时钟不同步锁过期时间计算会出错GC 停顿客户端获取锁后发生 Full GC停顿时间超过锁过期时间导致锁已释放但客户端不知情网络延迟获取锁的网络延迟不确定可能导致锁实际有效时间被压缩fencing token 缺失Redlock 没有提供 fencing token 机制无法防止客户端在锁过期后继续操作资源业界共识Redlock 在理论上有缺陷但在时钟同步、网络稳定、GC 可控的环境中其可靠性远高于单节点 Redis 锁。对于极端强一致场景建议使用 ZooKeeper 或 etcd 的分布式锁。3. Redisson 分布式锁底层实现详解3.1 加锁核心流程// RedissonLock.tryLockInnerAsync 的核心 Lua 脚本TRFutureTtryLockInnerAsync(longleaseTime,TimeUnitunit,longthreadId,RedisStrictCommandTcommand){internalLockLeaseTimeunit.toMillis(leaseTime);returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,command,// Lua 脚本尝试获取锁if (redis.call(exists, KEYS[1]) 0) then // 锁不存在redis.call(hincrby, KEYS[1], ARGV[2], 1); // 创建 Hashvalue1redis.call(pexpire, KEYS[1], ARGV[1]); // 设置过期时间return nil; end; if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then // 锁存在且是当前线程redis.call(hincrby, KEYS[1], ARGV[2], 1); // 重入次数1redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; return redis.call(pttl, KEYS[1]);,// 返回锁的剩余过期时间Collections.ObjectsingletonList(getName()),internalLockLeaseTime,getLockName(threadId));}参数说明KEYS[1]锁名如order:1001ARGV[1]过期时间毫秒ARGV[2]线程标识格式UUID:ThreadID3.2 解锁核心流程// RedissonLock.unlockInnerAsync 的核心 Lua 脚本protectedRFutureBooleanunlockInnerAsync(longthreadId){returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,RedisCommands.EVAL_BOOLEAN,// Lua 脚本释放锁if (redis.call(hexists, KEYS[1], ARGV[3]) 0) then // 不是当前线程持有的锁return nil;end; local counter redis.call(hincrby, KEYS[1], ARGV[3], -1); // 重入次数-1if (counter 0) then redis.call(pexpire, KEYS[1], ARGV[2]); // 仍持有锁续期return 0; else redis.call(del, KEYS[1]); // 完全释放删除锁redis.call(publish, KEYS[2], ARGV[1]); // 发布解锁通知return 1; end; return nil;,Arrays.ObjectasList(getName(),getChannelName()),LockPubSub.UNLOCK_MESSAGE,internalLockLeaseTime,getLockName(threadId));}关键设计可重入通过 Hash 结构的hincrby实现重入计数原子释放GET判断DEL 通过 Lua 脚本原子执行解锁通知释放锁后通过PUBLISH通知等待线程避免忙等待。3.3 看门狗续期机制// Redisson 看门狗定时任务简化逻辑privatevoidscheduleExpirationRenewal(longthreadId){TimeouttaskcommandExecutor.getConnectionManager().newTimeout(timeout-{RFutureBooleanfuturerenewExpirationAsync(threadId);future.onComplete((res,e)-{if(e!null)return;if(res){// 续期成功递归调度下一次续期scheduleExpirationRenewal(threadId);}});},internalLockLeaseTime/3,TimeUnit.MILLISECONDS);// 每 1/3 过期时间续期一次expirationRenewalMap.put(getEntryName(),task);}4. 分布式锁方案对比方案实现原理可重入自动续期阻塞重试主从一致性适用场景SETNX EXPIRE早期命令组合❌❌❌❌学习演示严禁生产SET NX EX Lua原子命令 脚本❌❌❌❌简单场景无重入需求Redisson 单节点Hash 看门狗 Pub/Sub✅✅✅❌大多数生产场景Redisson RedLock多节点多数派✅✅✅⚠️ 理论缺陷高可用要求场景ZooKeeper临时顺序节点✅✅会话续期✅Watcher✅强一致性场景etcdLease Revision✅✅✅✅云原生强一致场景5. 生产环境避坑指南5.1 严禁使用 SETNX EXPIRE 分离实现两个命令非原子中间崩溃即死锁。必须使用SET key value NX EX seconds。5.2 锁的过期时间必须大于业务最大执行时间如果业务执行时间不确定使用 Redisson 看门狗自动续期而非固定过期时间。5.3 避免锁粒度过粗lock:global会导致所有操作串行系统吞吐量暴跌。应按业务 ID 细粒度加锁。5.4 锁的 value 必须唯一使用UUID ThreadID组合避免不同 JVM 实例的线程 ID 冲突。5.5 解锁必须使用 Lua 脚本GETDEL 分离操作存在竞态条件必须通过 Lua 脚本原子执行。5.6 注意 Redlock 的局限性在时钟不同步、网络不稳定、GC 不可控的环境中Redlock 可靠性下降。极端强一致场景优先选择 ZooKeeper 或 etcd。5.7 监控与告警监控锁的持有时间超过阈值如 10 秒触发告警可能业务异常或死锁。监控 Redis 锁 Key 的数量突增可能意味着锁未释放死锁。看门狗续期次数过多时告警业务执行时间异常长。6. 面试官追问与高分回答模板追问 1“Redis 分布式锁怎么实现”低分回答“用 SETNX 加锁EXPIRE 设置过期时间DEL 释放锁。”有死锁和误删风险高分回答Redis 分布式锁的可靠实现需要三层保障原子加锁使用SET lock_key unique_value NX EX 30将加锁和设置过期时间合并为原子操作避免SETNX EXPIRE分离导致的死锁。防误删锁的 value 使用UUID:ThreadID唯一标识释放锁时通过 Lua 脚本原子执行’GET 判断归属 → DEL 删除’避免误删其他线程的锁。自动续期使用 Redisson 看门狗机制每 10 秒自动续期锁的过期时间防止业务执行超时导致锁提前释放。生产环境严禁手写 SETNX必须直接使用 Redisson 等成熟框架。追问 2“为什么释放锁必须用 Lua 脚本”高分回答因为’判断锁归属’和’删除锁’是两个独立操作非原子执行存在竞态条件线程 A 执行 GET 判断锁归属确认是自己的锁此时锁恰好过期线程 B 获取了同一锁线程 A 执行 DEL误删了线程 B 的锁。Lua 脚本在 Redis 服务端原子执行将 GET判断DEL 封装为单命令彻底消除这个竞态窗口。这是 Redis 分布式锁从’能用’到’可靠’的关键一步。追问 3“看门狗机制是什么怎么实现的”高分回答看门狗Watchdog是 Redisson 解决’业务执行时间超过锁过期时间’问题的核心机制。当调用lock.lock()不指定 leaseTime时Redisson 默认启用看门狗初始设置锁过期时间为 30 秒lockWatchdogTimeout后台启动定时任务每 10 秒leaseTime / 3检查一次锁是否仍被当前线程持有如果持有通过 Lua 脚本将锁的过期时间重新设置为 30 秒业务完成或线程中断时取消定时任务停止续期。这样即使业务执行 5 分钟锁也不会提前释放。如果显式指定leaseTime如lock.lock(10, TimeUnit.SECONDS)看门狗不会启用10 秒后强制释放。追问 4“Redlock 算法是什么有什么问题”高分回答Redlock 是 Redis 作者 antirez 提出的多节点分布式锁算法核心思想是在 N 个独立 Redis 实例上同时加锁如果成功节点数 N/2 1 且总耗时小于锁过期时间则获取锁成功。但 Martin Kleppmann 在论文中提出了几个关键质疑时钟漂移如果 Redis 节点时钟不同步锁过期时间计算会出错可能导致两个客户端同时认为持有锁GC 停顿客户端获取锁后发生 Full GC停顿时间超过锁过期时间锁已释放但客户端不知情继续操作数据fencing token 缺失Redlock 没有提供 fencing token单调递增的令牌客户端在锁过期后仍可能向资源服务器写入数据无法被拦截。业界共识是Redlock 在时钟同步、网络稳定、GC 可控的环境中可靠性足够但极端强一致场景应优先选择 ZooKeeper临时顺序节点 Watcher或 etcdLease 机制。追问 5“Redisson 的阻塞重试是怎么实现的”高分回答Redisson 的阻塞重试通过Pub/Sub 事件驱动实现而非忙等待首次获取锁失败时计算剩余等待时间订阅该锁的解锁频道redisson_lock__channel:{lockName}阻塞等待解锁通知或超时收到通知后再次尝试获取锁循环直到超时。这种设计的优势是无 CPU 空转线程在等待期间不消耗 CPU 资源。相比自旋锁忙等待事件驱动重试的 CPU 利用率极低适合高并发场景。追问 6“Redis 分布式锁和 ZooKeeper 分布式锁怎么选”高分回答选择取决于一致性要求和性能需求Redis 分布式锁Redisson性能极高QPS 可达万级实现简单适合大多数生产场景。但基于过期时间存在时钟漂移和 GC 停顿的理论风险属于最终一致性方案。ZooKeeper 分布式锁基于临时顺序节点和 Watcher 机制锁的释放与客户端会话绑定客户端崩溃会话过期自动释放不存在时钟漂移问题属于强一致性方案。但性能较低QPS 千级且需维护 ZK 集群。一般原则高并发、性能优先选 Redis强一致、可靠性优先选 ZooKeeper。在大多数互联网业务中Redisson 单节点锁已足够可靠Redlock 和 ZK 仅在极端场景使用。7. 方案选型速查表业务场景推荐方案核心理由通用分布式锁订单、库存Redisson 单节点可重入、自动续期、阻塞重试功能完备高可用要求Redis 主从切换频繁Redisson RedLock多节点多数派容忍部分节点故障极端强一致金融交易ZooKeeper / etcd会话绑定释放无时钟漂移风险简单场景无重入需求SET NX EX Lua轻量不依赖第三方框架学习/演示SETNX EXPIRE❌ 严禁生产使用面试官想要的满分总结Redis 分布式锁从能用到生产级可靠经历了三个演进阶段基础阶段SETNX EXPIRE→ 存在死锁和误删风险严禁生产使用可靠阶段SET NX EX原子加锁 Lua 脚本原子释放 唯一 value 防误删 → 解决死锁和误删但不可重入、无自动续期生产阶段RedissonHash 可重入 看门狗续期 Pub/Sub 阻塞重试→ 功能完备大多数场景首选。理解分布式锁必须抓住四个核心问题死锁原子加锁过期时间、误删Lua 脚本唯一 value、超时释放看门狗续期、主从一致性Redlock 或 ZK 替代。Redlock 算法虽有理论争议但在稳定环境中可靠性足够。极端强一致场景应优先选择 ZooKeeper 或 etcd。生产环境严禁手写分布式锁直接使用 Redisson 等成熟框架。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~