当前位置: 首页 > news >正文

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

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

一、什么是分布式锁

1.1 为什么需要分布式锁

在单机时代,synchronizedReentrantLock可以很好地解决多线程竞争问题——因为它们工作在同一进程内,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);// 插入或更新锁记录intupdated=jdbcTemplate.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);returnupdated>0;}catch(Exceptione){// 主键冲突或行锁等待超时returnfalse;}}/** * 释放锁 */@Transactionalpublicbooleanunlock(StringlockKey,StringrequestId){intdeleted=jdbcTemplate.update("DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?",lockKey,requestId);returndeleted>0;}}

优缺点分析

维度评价
✅ 优势实现简单,无需引入额外中间件;数据强一致(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){StringluaScript="if redis.call('GET', KEYS[1]) == ARGV[1] then "+" return redis.call('DEL', KEYS[1]) "+"else "+" return 0 "+"end";Longresult=redisTemplate.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 个(通常 N=5)独立的 Redis 节点上同时获取锁,只要超过半数(N/2 + 1)节点获取成功,就算获取锁成功。

/** * RedLock 算法核心逻辑 */publicclassRedLock{privatestaticfinalintNODES_COUNT=5;privatestaticfinallongCLOCK_DRIFT_FACTOR=0.01;// 时钟漂移因子privatefinalList<RedisLock>nodes;/** * 获取 RedLock 锁 */publicbooleantryLock(Stringkey,StringrequestId,longleaseTime,longwaitTime)throwsInterruptedException{longstartTime=System.currentTimeMillis();intsuccessfulCount=0;// ⭐ 阶段1:逐个尝试所有节点for(RedisLocknode:nodes){// 每个节点的获取超时 = 总等待时间 / 节点数longnodeWaitTime=waitTime/NODES_COUNT;if(node.tryLock(key,requestId,leaseTime,nodeWaitTime)){successfulCount++;}}// ⭐ 阶段2:检查是否超过半数 + 是否超时longelapsedTime=System.currentTimeMillis()-startTime;longdrift=(long)(leaseTime*CLOCK_DRIFT_FACTOR)+2;// 时钟漂移补偿if(successfulCount>=NODES_COUNT/2+1&&elapsedTime<leaseTime-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;privatefinalAtomicLongtokenGenerator=newAtomicLong(0);publicFencedLockacquireLock(Stringkey){longtoken=tokenGenerator.incrementAndGet();booleanlocked=redisTemplate.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 生产级方案:Redisson

Redisson 在基础 Redis 锁的基础上,增加了看门狗自动续期、可重入、订阅通知等机制,是生产环境中使用最广泛的方案(已在系列前篇中详细展开,此处不再重复)。

2.3 基于 ZooKeeper 的分布式锁

2.3.1 原理:临时顺序节点 + Watcher

ZooKeeper 利用其临时顺序节点的特性,天然适合实现分布式锁:

/locks/my_lock/ ← 持久节点(锁的根路径) ├── lock_0000000001 ← 临时顺序节点(请求最早) ├── lock_0000000002 ← 临时顺序节点 ├── lock_0000000003 ← 临时顺序节点(请求最晚)

锁获取规则

  • 所有客户端在/locks/my_lock/下创建临时顺序节点
  • 创建完成后,检查自己的节点序号是否为最小
  • 如果是 → 获取锁成功
  • 如果不是 → 监听前一个节点的删除事件,进入等待
2.3.2 Curator 实现
publicclassZooKeeperDistributedLock{privatefinalCuratorFrameworkclient;publicZooKeeperDistributedLock(StringconnectString){this.client=CuratorFrameworkFactory.builder().connectString(connectString).sessionTimeoutMs(60000).connectionTimeoutMs(15000).retryPolicy(newExponentialBackoffRetry(1000,3)).build();this.client.start();}/** * 使用 Curator 的 InterProcessMutex 实现分布式锁 */publicvoidexecuteWithLock(StringlockPath,Runnabletask){InterProcessMutexlock=newInterProcessMutex(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){InterProcessReadWriteLockrwLock=newInterProcessReadWriteLock(client,lockPath);InterProcessLockreadLock=rwLock.readLock();// 使用方式同 InterProcessMutex}}
2.3.3 ZK vs Redis 分布式锁对比
维度ZooKeeperRedis
一致性模型强一致(ZAB 协议)最终一致(主从异步复制)
锁自动释放临时节点,会话断开即删除通过过期时间实现
死锁防护✅ 天然防死锁(会话超时)✅ 过期时间防护
性能中(磁盘写 + 选举)高(内存操作)
可重入✅ Curator 支持✅ Redisson 支持
公平性✅ 天然公平(按节点序号)❌ 默认非公平
客户端复杂度高(需管理会话)
羊群效应有(但 Curator 已优化为只监听前一个节点)无(不涉及 Watcher)

三、三种方案的全面对比

对比维度数据库方案Redis 方案ZooKeeper 方案
实现难度⭐ 低⭐⭐ 中⭐⭐⭐ 高
性能⭐ 低(毫秒级)⭐⭐⭐ 高(微秒级)⭐⭐ 中(毫秒级)
可靠性⭐⭐ 中(DB 故障风险)⭐⭐ 中(主从切换丢锁)⭐⭐⭐ 高(ZAB 强一致)
可用性⭐⭐ 中⭐⭐⭐ 高(哨兵/集群)⭐⭐⭐ 高(集群选举)
自动续期❌ 需自行实现✅ Redisson 看门狗❌ 但会话超时自动释放
防死锁✅ 过期删除✅ 过期时间✅ 临时节点
运维成本⭐ 低(复用 DB)⭐⭐ 中(需维护 Redis)⭐⭐⭐ 高(需维护 ZK 集群)
适用规模中到大中到大
典型延迟1-10ms<1ms1-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

推荐:Redisson(Redis 方案)

理由:

✅ 已引入 Redis,零额外成本

✅ 性能高,微秒级延迟

✅ 看门狗自动续期,无需担心任务超时

✅ 支持可重入、公平锁、读写锁

❌ 极端情况(主从切换)可能丢锁

适用:大多数业务场景(订单、支付、任务调度)

场景二:金融级场景,数据一致性优先

推荐:ZooKeeper / Curator

理由:

✅ 强一致保证,锁信息绝不丢失

✅ 临时节点自动释放,无超时窗口

✅ 天然公平排队

❌ 性能低于 Redis

❌ 运维复杂度高

适用:分布式事务协调、选主、配置同步

场景三:简单互斥,不想引入中间件

推荐:数据库行锁(SELECT … FOR UPDATE)

理由:

✅ 复用现有数据库,零额外成本

✅ 事务保障,数据强一致

❌ 性能差,不适合高并发

❌ 数据库连接池可能被锁请求耗尽

适用:后台管理任务、定时报表、低并发互斥

场景四:高并发 + 可接受最终一致

推荐:Redis + 本地锁双检

理由:

✅ Redis 高性能处理大部分请求

✅ 本地锁缓存去重,减少 Redis 压力

❌ 极端情况可能有短暂不一致

适用:秒杀、活动、热点数据防护

// 双检锁模式(本地 + 分布式)publicclassHybridLock{privatefinalReentrantLocklocalLock=newReentrantLock();privatefinalRLockredisLock;publicvoidexecute(Runnabletask){// ⭐ 第一层:本地锁(快速失败)if(!localLock.tryLock()){thrownewRuntimeException("系统繁忙");}try{// ⭐ 第二层:分布式锁if(!redisLock.tryLock()){thrownewRuntimeException("系统繁忙");}try{task.run();}finally{redisLock.unlock();}}finally{localLock.unlock();}}}

六、分布式锁的常见陷阱

陷阱一:锁超时导致并发

// ❌ 问题:锁过期时间太短RLocklock=redisson.getLock("myLock");lock.lock(5,TimeUnit.SECONDS);// 5 秒后自动释放// 业务执行了 10 秒 → 5 秒时锁已释放,其他线程进入// ✅ 正确:使用看门狗自动续期lock.lock();// 不指定 leaseTime,启用看门狗// 或者设置足够长的租约时间lock.lock(60,TimeUnit.SECONDS);// 确保业务能在 60 秒内完成

陷阱二:锁 Key 设计不当

// ❌ 问题:Key 粒度过粗@DistributedLock(key="'lock:order'")// 所有订单共享一把锁// → 所有订单处理串行化,性能极差// ❌ 问题:Key 粒度过细@DistributedLock(key="'lock:order:item:' + #itemId")// → 同一订单的不同商品可以同时处理,但可能逻辑上需要互斥// ✅ 正确:Key 粒度与业务逻辑匹配@DistributedLock(key="'lock: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 可靠但不快,数据库简单但不高性能。理解业务对一致性的真实需求,比选择"最好的"锁方案更重要。

http://www.cnnetsun.cn/news/3602061.html

相关文章:

  • Cookie实现Web选项卡状态持久化方案
  • 肿瘤微环境——IFN-γ/IL-10/IL-18/IL-1α/IL-1β/IL-21/IL-6/TNF-α/VEGF-A Panel,重新定义免疫-血管-炎症的联合检测
  • Linux基础命令与开发入门
  • iPad Pro M2运行Win11 Pro的UTM虚拟机完整指南
  • 三星 Galaxy Watch 9 与 Galaxy Watch 8 对比:谁更适合你?
  • 告别噪音与频繁维护:凯尼克静音皮带模组重塑车间
  • ARM Cortex-M时钟门控技术:RCGC/SCGC/DCGC寄存器详解与低功耗实战
  • 2026户外照明商城小程序开发十大平台测评:场景内容、社群与会员怎么选?含零代码SAAS、AI编程、源码定制交付
  • AI系统架构设计:从数据处理到模型部署实战
  • 三星新折叠屏手机首用硅碳电池:小空间大容量,但容量不及OPPO、荣耀!
  • 外贸开发信避坑与选型:一份给业务员的实操清单
  • 托育中心低成本获客神器,凡科全新1折优惠渠道:99做小程序只认餐宝盈,含零代码SAAS、AI编程、源码定制交付
  • 房地产电子沙盘能提高多少转化率?
  • SolidWorks实体合并技巧与焊件处理实战
  • 飞书AI自动化流程实战手册:7类高频场景模板+5个避坑红线,今天部署明天见效
  • 神经网络架构搜索(NAS)技术原理与应用实践
  • AI Agent架构设计与性能优化实战指南
  • AI写作工具在文学研究中的应用与伦理探讨
  • CNN-GRU-SE混合模型在时序数据分类中的应用与实现
  • AI答辩助手:毕业季论文格式与答辩模拟全攻略
  • 多层神经网络(MLP)原理与工程实践详解
  • AI工具助力论文查重降重实战指南
  • SD提示词反推技术白皮书(2024最新版):基于127个真实生成图的语义熵分析与权重还原算法
  • VMware虚拟机搭建CentOS开发环境完整指南
  • Tiva™微控制器低功耗设计:DCGCx与PCx寄存器实战配置指南
  • AI算命类决策应用的用户痛点与选型分析
  • WT7015三功能手电筒芯片WT7015
  • 2026江苏公考备战,选对“封闭式基地班“到底有多重要?
  • html全国全域城市旅游客流迁徙大屏
  • 分布式定时任务调度:核心原理与生产实践