Java 锁机制深度解析(系列六):分布式锁

📅 2026/7/23 13:19:02
Java 锁机制深度解析(系列六):分布式锁
Java 锁机制深度解析系列六分布式锁一、什么是分布式锁1.1 为什么需要分布式锁在单机时代synchronized和ReentrantLock可以很好地解决多线程竞争问题——因为它们工作在同一进程内JVM 或 AQS 可以协调线程的访问顺序。但在分布式系统中情况发生了变化1.2 分布式锁的定义分布式锁是一种跨多个服务实例进程的同步机制用于协调对共享资源的互斥访问。它的核心特征与单机锁类似但需要解决网络不确定性带来的额外挑战。1.3 分布式锁需要满足的条件必要条件① 互斥性同一时刻只有一个客户端持有锁② 防死锁持有锁的客户端崩溃后锁能自动释放③ 可重入同一客户端可重复获取同一把锁④ 高可用锁服务本身不能成为单点故障⑤ 高性能获取和释放锁的开销应尽可能低​进阶条件⑥ 公平性按请求顺序获取锁可选⑦ 容错性部分节点故障不影响锁的正确性⑧ 可续期长时间任务可延长锁持有时间⑨ 可监控支持查看谁持有锁、等待队列等信息​二、分布式锁的实现方案目前主流的分布式锁实现方案有三种方案代表实现一致性保障性能复杂度基于数据库数据库行锁 / 乐观锁强一致低低基于 RedisSET NX / Redisson / RedLock最终一致高中基于 ZooKeeperCurator / 临时顺序节点强一致中高2.1 基于数据库的分布式锁2.1.1 方案一数据库行锁悲观锁-- 方案使用 SELECT ... FOR UPDATE 实现互斥-- 前提需要有一张锁表CREATETABLEdistributed_lock(lock_keyVARCHAR(128)PRIMARYKEY,lock_valueVARCHAR(64)NOTNULLCOMMENT锁持有者标识,expire_atDATETIMENOTNULLCOMMENT锁过期时间,created_atDATETIMEDEFAULTCURRENT_TIMESTAMP);​ServicepublicclassDatabaseDistributedLock{AutowiredprivateJdbcTemplatejdbcTemplate;/** * 获取锁基于数据库行锁 */TransactionalpublicbooleantryLock(StringlockKey,StringrequestId,longexpireSeconds){try{// ⭐ FOR UPDATE 锁定行其他事务必须等待// 如果记录不存在INSERT 也会隐式加锁jdbcTemplate.queryForObject(SELECT lock_value FROM distributed_lock WHERE lock_key ? FOR UPDATE,String.class,lockKey);// 检查是否已过期防止客户端崩溃导致锁不释放jdbcTemplate.update(DELETE FROM distributed_lock WHERE lock_key ? AND expire_at NOW(),lockKey);// 插入或更新锁记录intupdatedjdbcTemplate.update(INSERT INTO distributed_lock(lock_key, lock_value, expire_at) VALUES(?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND)) ON DUPLICATE KEY UPDATE lock_value VALUES(lock_value), expire_at VALUES(expire_at),lockKey,requestId,expireSeconds);returnupdated0;}catch(Exceptione){// 主键冲突或行锁等待超时returnfalse;}}/** * 释放锁 */Transactionalpublicbooleanunlock(StringlockKey,StringrequestId){intdeletedjdbcTemplate.update(DELETE FROM distributed_lock WHERE lock_key ? AND lock_value ?,lockKey,requestId);returndeleted0;}}优缺点分析维度评价✅ 优势实现简单无需引入额外中间件数据强一致ACID 事务保障❌ 劣势性能差行锁 IO数据库连接成为瓶颈存在单点故障风险适用场景低并发、对一致性要求极高的场景如财务对账2.1.2 方案二乐观锁版本号-- 不单独使用锁表而是在业务表上加版本号字段ALTERTABLEinventoryADDCOLUMNversionINTDEFAULT0;​// 乐观锁更新CAS 思想Update(UPDATE inventory SET stock stock - #{count}, version version 1 WHERE id #{goodsId} AND stock #{count} AND version #{oldVersion})intdeductStock(Param(goodsId)LonggoodsId,Param(count)intcount,Param(oldVersion)intoldVersion);适用场景更新冲突概率较低的简单场景如配置更新。2.2 基于 Redis 的分布式锁2.2.1 基础方案SET NX EX/** * Redis 分布式锁的基础实现 * 核心命令SET key value NX EX timeout */publicclassRedisDistributedLock{privatefinalStringRedisTemplateredisTemplate;/** * 获取锁 */publicbooleantryLock(Stringkey,StringrequestId,longexpireMs){returnBoolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key,requestId,expireMs,TimeUnit.MILLISECONDS));}/** * 释放锁Lua 脚本保证原子性 */publicbooleanreleaseLock(Stringkey,StringrequestId){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(key),requestId);returnLong.valueOf(1).equals(result);}}这个基础方案存在的问题问题1锁超时自动释放?t0: 线程A 获取锁过期时间 10 秒 t1: 线程A 业务执行时间超过 10 秒 t2: 锁自动释放 t3: 线程B 获取锁认为锁空闲 t4: 线程A 业务完成释放锁 → 释放了线程B 的锁 t5: 线程C 获取锁 → 线程B 和线程C 同时持有锁问题2主从切换锁丢失?线程A 获取锁成功主节点 主节点宕机锁信息未同步到从节点 从节点升级为主节点没有锁记录 线程B 获取锁成功 → 线程A 和线程B 同时持有锁​2.2.2 进阶方案RedLock 算法RedLock 由 Redis 作者 Antirez 提出用于解决 Redis 主从切换的锁丢失问题。核心思想在 N 个通常 N5独立的 Redis 节点上同时获取锁只要超过半数N/2 1节点获取成功就算获取锁成功。/** * RedLock 算法核心逻辑 */publicclassRedLock{privatestaticfinalintNODES_COUNT5;privatestaticfinallongCLOCK_DRIFT_FACTOR0.01;// 时钟漂移因子privatefinalListRedisLocknodes;/** * 获取 RedLock 锁 */publicbooleantryLock(Stringkey,StringrequestId,longleaseTime,longwaitTime)throwsInterruptedException{longstartTimeSystem.currentTimeMillis();intsuccessfulCount0;// ⭐ 阶段1逐个尝试所有节点for(RedisLocknode:nodes){// 每个节点的获取超时 总等待时间 / 节点数longnodeWaitTimewaitTime/NODES_COUNT;if(node.tryLock(key,requestId,leaseTime,nodeWaitTime)){successfulCount;}}// ⭐ 阶段2检查是否超过半数 是否超时longelapsedTimeSystem.currentTimeMillis()-startTime;longdrift(long)(leaseTime*CLOCK_DRIFT_FACTOR)2;// 时钟漂移补偿if(successfulCountNODES_COUNT/21elapsedTimeleaseTime-drift){// 获取成功有效持有时间 原始租期 - 已消耗时间returntrue;}// ⭐ 阶段3获取失败释放所有节点上的锁for(RedisLocknode:nodes){node.releaseLock(key,requestId);}returnfalse;}}RedLock 的争议观点代表人物核心论据✅ 支持Redis 作者 Antirez在工程实践中时钟漂移可控RedLock 足够安全❌ 反对分布式系统专家 Martin KleppmannRedLock 依赖时钟同步本质上是 fencing token 机制才是正确方案Martin 提出的替代方案使用fencing token单调递增的令牌// ⭐ fencing token 方案// 1. 锁服务分配一个单调递增的 token// 2. 每次写操作携带 token// 3. 资源端拒绝 token 小于当前最大值的请求// 示例带 fencing token 的分布式锁publicclassFencingTokenLock{privatefinalRedisTemplateredisTemplate;privatefinalAtomicLongtokenGeneratornewAtomicLong(0);publicFencedLockacquireLock(Stringkey){longtokentokenGenerator.incrementAndGet();booleanlockedredisTemplate.opsForValue().setIfAbsent(key,String.valueOf(token),30,TimeUnit.SECONDS);if(locked){returnnewFencedLock(key,token);}returnnull;}// 使用时所有写操作都必须携带 tokenpublicvoidwriteData(FencedLocklock,Objectdata){// ⭐ 资源端校验token 必须大于最后一次写入的 token// 即使锁已经超时释放过期的请求也会被 token 机制拦截dataStore.writeWithToken(lock.getToken(),data);}}2.2.3 生产级方案RedissonRedisson 在基础 Redis 锁的基础上增加了看门狗自动续期、可重入、订阅通知等机制是生产环境中使用最广泛的方案已在系列前篇中详细展开此处不再重复。2.3 基于 ZooKeeper 的分布式锁2.3.1 原理临时顺序节点 WatcherZooKeeper 利用其临时顺序节点的特性天然适合实现分布式锁/locks/my_lock/ ← 持久节点锁的根路径 ├── lock_0000000001 ← 临时顺序节点请求最早 ├── lock_0000000002 ← 临时顺序节点 ├── lock_0000000003 ← 临时顺序节点请求最晚锁获取规则所有客户端在/locks/my_lock/下创建临时顺序节点创建完成后检查自己的节点序号是否为最小如果是 → 获取锁成功如果不是 → 监听前一个节点的删除事件进入等待2.3.2 Curator 实现publicclassZooKeeperDistributedLock{privatefinalCuratorFrameworkclient;publicZooKeeperDistributedLock(StringconnectString){this.clientCuratorFrameworkFactory.builder().connectString(connectString).sessionTimeoutMs(60000).connectionTimeoutMs(15000).retryPolicy(newExponentialBackoffRetry(1000,3)).build();this.client.start();}/** * 使用 Curator 的 InterProcessMutex 实现分布式锁 */publicvoidexecuteWithLock(StringlockPath,Runnabletask){InterProcessMutexlocknewInterProcessMutex(client,lockPath);try{// ⭐ 尝试获取锁可设置超时if(lock.acquire(10,TimeUnit.SECONDS)){try{task.run();}finally{lock.release();// 释放锁 → 删除临时节点}}else{thrownewRuntimeException(获取分布式锁超时);}}catch(Exceptione){Thread.currentThread().interrupt();thrownewRuntimeException(e);}}/** * 读写锁实现 */publicvoidexecuteWithReadLock(StringlockPath,Runnabletask){InterProcessReadWriteLockrwLocknewInterProcessReadWriteLock(client,lockPath);InterProcessLockreadLockrwLock.readLock();// 使用方式同 InterProcessMutex}}2.3.3 ZK vs Redis 分布式锁对比维度ZooKeeperRedis一致性模型强一致ZAB 协议最终一致主从异步复制锁自动释放临时节点会话断开即删除通过过期时间实现死锁防护✅ 天然防死锁会话超时✅ 过期时间防护性能中磁盘写 选举高内存操作可重入✅ Curator 支持✅ Redisson 支持公平性✅ 天然公平按节点序号❌ 默认非公平客户端复杂度高需管理会话低羊群效应有但 Curator 已优化为只监听前一个节点无不涉及 Watcher三、三种方案的全面对比对比维度数据库方案Redis 方案ZooKeeper 方案实现难度⭐ 低⭐⭐ 中⭐⭐⭐ 高性能⭐ 低毫秒级⭐⭐⭐ 高微秒级⭐⭐ 中毫秒级可靠性⭐⭐ 中DB 故障风险⭐⭐ 中主从切换丢锁⭐⭐⭐ 高ZAB 强一致可用性⭐⭐ 中⭐⭐⭐ 高哨兵/集群⭐⭐⭐ 高集群选举自动续期❌ 需自行实现✅ Redisson 看门狗❌ 但会话超时自动释放防死锁✅ 过期删除✅ 过期时间✅ 临时节点运维成本⭐ 低复用 DB⭐⭐ 中需维护 Redis⭐⭐⭐ 高需维护 ZK 集群适用规模小中到大中到大典型延迟1-10ms1ms1-5ms性能基准数据粗略参考场景10 个客户端同时竞争一把锁每个客户端持有锁 100ms 数据库方案约 200 TPS ← 数据库连接和行锁是瓶颈 Redis 方案约 5000 TPS ← 内存操作高吞吐 ZK 方案 约 800 TPS ← ZAB 协议写入延迟四、方案选择决策树需要分布式锁吗 ├── 可以接受偶尔的并发问题 │ └── → 乐观锁数据库版本号 / CAS │ ├── 并发量低 100 TPS │ ├── 已有 MySQL 且不想引入新中间件 → 数据库行锁 │ └── 对一致性要求极高 → ZooKeeper │ ├── 并发量中高100-10000 TPS │ ├── 可以接受极端情况下的锁丢失 → Redis SET NX基础方案 │ ├── 需要自动续期、可重入 → Redisson推荐 │ └── 完全不能接受锁丢失 → ZooKeeper / RedLock │ └── 超高并发 10000 TPS ├── 是否可以优化为无锁→ 重试 / 异步队列 ├── 是否可以接受近似互斥→ Lua 乐观锁 └── 必须精确互斥 → Redisson 本地缓存降级五、生产选型建议场景一中小项目已使用 Redis推荐RedissonRedis 方案理由✅ 已引入 Redis零额外成本✅ 性能高微秒级延迟✅ 看门狗自动续期无需担心任务超时✅ 支持可重入、公平锁、读写锁❌ 极端情况主从切换可能丢锁适用大多数业务场景订单、支付、任务调度场景二金融级场景数据一致性优先推荐ZooKeeper / Curator理由✅ 强一致保证锁信息绝不丢失✅ 临时节点自动释放无超时窗口✅ 天然公平排队❌ 性能低于 Redis❌ 运维复杂度高适用分布式事务协调、选主、配置同步场景三简单互斥不想引入中间件推荐数据库行锁SELECT … FOR UPDATE理由✅ 复用现有数据库零额外成本✅ 事务保障数据强一致❌ 性能差不适合高并发❌ 数据库连接池可能被锁请求耗尽适用后台管理任务、定时报表、低并发互斥场景四高并发 可接受最终一致推荐Redis 本地锁双检理由✅ Redis 高性能处理大部分请求✅ 本地锁缓存去重减少 Redis 压力❌ 极端情况可能有短暂不一致适用秒杀、活动、热点数据防护// 双检锁模式本地 分布式publicclassHybridLock{privatefinalReentrantLocklocalLocknewReentrantLock();privatefinalRLockredisLock;publicvoidexecute(Runnabletask){// ⭐ 第一层本地锁快速失败if(!localLock.tryLock()){thrownewRuntimeException(系统繁忙);}try{// ⭐ 第二层分布式锁if(!redisLock.tryLock()){thrownewRuntimeException(系统繁忙);}try{task.run();}finally{redisLock.unlock();}}finally{localLock.unlock();}}}六、分布式锁的常见陷阱陷阱一锁超时导致并发// ❌ 问题锁过期时间太短RLocklockredisson.getLock(myLock);lock.lock(5,TimeUnit.SECONDS);// 5 秒后自动释放// 业务执行了 10 秒 → 5 秒时锁已释放其他线程进入// ✅ 正确使用看门狗自动续期lock.lock();// 不指定 leaseTime启用看门狗// 或者设置足够长的租约时间lock.lock(60,TimeUnit.SECONDS);// 确保业务能在 60 秒内完成陷阱二锁 Key 设计不当// ❌ 问题Key 粒度过粗DistributedLock(keylock:order)// 所有订单共享一把锁// → 所有订单处理串行化性能极差// ❌ 问题Key 粒度过细DistributedLock(keylock:order:item: #itemId)// → 同一订单的不同商品可以同时处理但可能逻辑上需要互斥// ✅ 正确Key 粒度与业务逻辑匹配DistributedLock(keylock:order: #orderId)// → 同一订单互斥不同订单并行陷阱三锁未正确释放// ❌ 问题未在 finally 中释放publicvoidwrong(){lock.lock();// 如果这里抛异常锁永远不会释放doSomething();lock.unlock();}// ✅ 正确publicvoidcorrect(){lock.lock();try{doSomething();}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}陷阱四锁的可重入问题// ❌ 问题基础 Redis SET NX 不支持重入publicvoidmethodA(){redisLock.lock(lockKey);methodB();// methodB 也尝试获取同一把锁 → 死锁redisLock.unlock(lockKey);}publicvoidmethodB(){redisLock.lock(lockKey);// SET NX 返回 false → 一直等待// ...redisLock.unlock(lockKey);}// ✅ 解决使用支持重入的 RedissonpublicvoidmethodA(){redissonLock.lock();methodB();// 重入成功redissonLock.unlock();}陷阱五Redis 主从切换丢锁时间线 t0: 线程A 向 Redis 主节点获取锁成功 t1: 主节点宕机锁未同步到从节点 t2: 从节点升级为主节点没有锁记录 t3: 线程B 获取同一把锁成功 t4: 线程A 和线程B 同时认为自己持有锁 解决方案 ① 使用 RedLock多节点独立 Redis ② 使用 ZooKeeper强一致 ③ 业务层做幂等兜底fencing token七、总结核心要点回顾知识点一句话总结分布式锁的本质跨进程的互斥机制解决单机锁无法协调多节点的问题数据库方案简单但性能差适合低并发场景Redis 方案高性能但存在主从切换丢锁的风险ZooKeeper 方案强一致但性能低于 Redis运维成本高RedLock通过多数派决策提高 Redis 锁的可靠性但存在争议看门狗自动续期机制解决任务超时导致锁释放的问题fencing token从资源端保证写入安全即使锁已经超时双检锁本地锁 分布式锁结合减少分布式锁压力最终选型建议有 Redis → Redisson看门狗 可重入 丰富功能强一致 → ZooKeeper临时节点 Watcher 机制零成本 → 数据库FOR UPDATE / 乐观锁高可靠 → RedLock多数派至少 3 节点极致性能 → Redis Lua简单场景自己实现没有银弹分布式锁没有完美的方案。Redis 快但不绝对可靠ZooKeeper 可靠但不快数据库简单但不高性能。理解业务对一致性的真实需求比选择最好的锁方案更重要。