一、业务背景:交易/库存扣减系统中的并发问题
在电商交易系统中,库存扣减是最典型的并发资源争抢场景。以秒杀活动为例,当数万用户同时抢购少量商品时,系统面临的核心挑战是超卖风险------多个并发请求同时查询库存并扣减,导致库存扣减数超过实际库存量。
典型的错误实现如下:
@GetMapping("/reduce")
public String reduce() {
int stock = (Integer) redisTemplate.opsForValue().get("stock");
if (stock > 0) {
redisTemplate.opsForValue().set("stock", stock - 1);
return "秒杀成功";
}
return "库存不足";
}
上述代码在并发场景下必然出现超卖。当用户A和用户B同时查询库存(均为10),用户A先扣减5个库存成功,用户B随后扣减6个库存,库存变为-1,即发生了超卖。
问题根源 在于:在分布式部署的多服务节点环境下,单机锁(如synchronized、ReentrantLock)仅能限制当前JVM内部的线程互斥,无法跨多个服务节点协调对共享资源的访问。当两个不同节点的进程同时扣减同一件商品的库存时,就必然导致数据一致性问题。
二、三种分布式锁方案详细对比
1. 基于数据库的分布式锁
实现原理 :利用数据库的唯一性约束或行锁机制。常见做法有两种:一是创建锁表并对lock_key字段建立唯一索引,通过INSERT成功获取锁、DELETE释放锁;二是使用SELECT ... FOR UPDATE对某一行数据加排他锁。
优点:
-
实现简单,无需引入额外中间件,开发成本极低
-
依赖数据库事务特性,数据可靠性高
-
连接断开后数据库自动回滚事务释放锁
缺点:
-
性能瓶颈:依赖磁盘I/O,高并发下数据库连接池资源会被迅速耗尽,拖垮整个DB
-
死锁风险 :基于
INSERT的方案如果服务宕机,锁无法自动释放,需额外开发定时清理任务 -
无超时机制:没有内置的过期机制,节点挂了锁就卡住了
-
非阻塞,获取失败直接返回,难以实现等待获取逻辑
适用场景:低频的管理后台操作或对性能不敏感的离线任务调度。在现代高并发场景下,纯粹基于数据库的分布式锁已基本被淘汰。
2. 基于Redis的分布式锁
实现原理 :利用Redis的SET key value NX PX milliseconds原子命令实现加锁------NX保证key不存在时才设置(互斥),PX设置过期时间(防死锁)。释放锁时必须使用Lua脚本保证原子性,先校验锁的持有者身份再删除。
-- 释放锁的Lua脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
优点:
-
性能极高:纯内存操作,QPS可达十万级,延迟在毫秒级。实测吞吐量是ZooKeeper的4倍、数据库的7倍
-
功能丰富:通过Redisson框架支持可重入锁、公平锁、读写锁、联锁等复杂场景
-
自动续期:Redisson的看门狗(Watchdog)机制可在业务未完成时自动延长锁超时时间
缺点:
-
一致性弱:Redis属于AP模型(优先保证可用性),主从异步复制可能导致锁数据丢失
-
集群故障风险:主从切换时,锁数据若未同步到从节点,新主节点上锁丢失,导致多个客户端同时持有锁
-
Redlock算法存在争议(如Martin Kleppmann的批评),且依赖时钟同步
适用场景:高并发秒杀、抢红包等对性能要求极高、对极端一致性要求可适当放宽的场景。推荐优先使用Redis配合Redisson满足大多数高并发场景。
3. 基于ZooKeeper的分布式锁
实现原理:利用ZooKeeper的临时顺序节点实现。每个客户端在锁目录下创建临时顺序节点,编号最小的节点获得锁,其他客户端通过Watch机制监听前序节点的删除事件。客户端会话断开或超时后,临时节点自动删除,天然解决死锁问题。
优点:
-
强一致性:基于ZAB协议(ZooKeeper Atomic Broadcast),保证集群数据强一致
-
天然防死锁:客户端异常断连后,ZooKeeper自动清理临时节点
-
公平锁:通过顺序节点天然实现先到先得的公平排队
-
原生支持可重入(同一Session内可重复获取)
缺点:
-
性能较低:频繁创建和删除节点对ZK集群压力较大,需经过共识协议
-
实现相对复杂,需依赖Curator等框架
-
存在广播风暴风险
-
部署成本和运维复杂度较高
适用场景:金融交易、分布式事务协调等对一致性要求极高的场景。当一致性需求高于性能需求时,ZooKeeper是更稳妥的选择。
综合对比表
| 特性 | Redis | ZooKeeper | 数据库(MySQL) |
|---|---|---|---|
| 性能 | 最高(内存操作) | 中等(共识协议) | 最低(磁盘I/O) |
| 一致性保证 | 弱(AP,异步复制) | 强(CP,ZAB协议) | 强(数据库事务) |
| 防死锁机制 | Key过期时间 | 临时节点自动删除 | 需额外定时清理 |
| 实现复杂度 | 中等 | 较低(Curator封装) | 低 |
| 锁唤醒机制 | Pub/Sub或自旋 | Watch事件通知 | 主动轮询 |
| 可重入性 | 需客户端实现 | 原生支持 | 需客户端实现 |
| 主要风险 | 主从切换锁失效 | 性能瓶颈、广播风暴 | 性能最差、易死锁 |
三、核心问题与解决方案
1. 锁超时问题
问题描述:业务执行时间超过锁的过期时间,锁被自动释放,但业务仍在进行中,导致其他线程获取锁后可能破坏数据一致性。
解决方案:
-
看门狗机制:Redisson的看门狗默认每10秒续期30秒,在业务未完成时持续延长锁有效期
-
合理预估超时:根据业务平均耗时设置合理的锁超时时间,避免客户端崩溃后锁一直无法释放
-
手动续期:自行实现定时续期线程,定期检查并延长锁的过期时间
2. 死锁问题
问题描述:客户端获取锁后因崩溃、网络中断等原因未能释放锁,导致其他客户端永远无法获取锁。
解决方案:
-
Redis方案:设置合理的TTL(过期时间),确保锁最终自动释放
-
ZooKeeper方案:利用临时节点特性,会话断开后自动删除节点
-
数据库方案:需额外开发定时任务清理过期锁记录
-
Redisson看门狗:既解决超时问题,也通过自动续期避免因业务耗时过长导致的"伪死锁"
3. 集群故障与锁失效问题
这是Redis分布式锁最致命的问题。场景复现:
-
客户端A向Redis主节点获取锁成功
-
主节点尚未将锁数据同步到从节点,突然宕机
-
哨兵将某个从节点提升为新主节点
-
新主节点中不存在该锁,客户端B成功获取锁
-
结果:客户端A和B同时持有同一把锁,互斥性被破坏
解决方案------RedLock算法:
-
部署多个(通常5个)独立的Redis主节点
-
客户端向所有节点依次发起加锁请求
-
只有当超过半数(N/2+1) 的节点成功获取锁,且总耗时小于锁的过期时间,才认为加锁成功
-
锁的实际有效时间 = 初始有效时间 - 获取锁的总耗时
-
如果加锁失败,客户端向所有Redis实例发起释放锁请求
⚠️ 注意:RedLock并非银弹。它强依赖多个节点的时钟同步,一旦有节点时钟发生错误,算法模型就失效了。同时,Martin Kleppmann曾对RedLock提出过批评,认为其在某些场景下仍不够安全。
四、混合架构解决方案
在实际生产环境中,单一方案难以同时满足性能、一致性和可用性的全部要求。以下是在交易/库存扣减系统中实践的混合架构方案:
方案设计:缓存锁先行 + 数据库锁兜底
核心思路:利用Redis的高性能处理99%的正常请求,同时通过数据库的强一致性作为最终兜底,确保数据绝对准确。
分层架构:
第一层:Redis分布式锁(Redisson实现)
-
使用Redisson的
RLock获取分布式锁,配合看门狗自动续期 -
加锁时设置合理的等待时间和超时时间
-
锁的value存储唯一客户端ID(UUID+线程ID),释放时校验身份,防止误删
第二层:Lua脚本原子扣减
-
将"检查库存+扣减库存"封装为Lua脚本在Redis中原子执行
-
避免"查-判-扣"三步操作之间的时间窗口被其他请求插入
第三层:数据库乐观锁兜底
-
在数据库层面使用
version字段实现乐观锁 -
更新时检查
WHERE version = old_version,若版本号不匹配则重试或失败 -
确保最终落库数据绝对准确
第四层:库存预占机制
-
用户下单时先预占库存(将库存从"可售"转为"预占"状态)
-
支付成功后再正式扣减,支付失败则释放预占
-
配合分布式锁保证预占操作的原子性
应急与容灾机制
-
降级方案 :当Redis集群完全不可用时,直接降级到数据库悲观锁(
SELECT ... FOR UPDATE),虽然性能下降但保证业务可用 -
监控告警:监控锁的持有时间、等待队列长度、超时次数等指标,异常时及时告警
-
滞留锁清理:运维层面准备批量清理固定前缀滞留锁key的脚本,紧急情况下快速恢复
实践效果
通过上述混合架构方案,库存扣减系统实现了:
-
性能:QPS从传统方案的120提升至8,200以上
-
准确性:彻底杜绝超卖,库存扣减成功率达到100%
-
可用性:任一环节故障均有降级方案,系统整体可用性显著提升
五、选型建议
综合以上分析,分布式锁的选型可参考以下原则:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 高并发秒杀、抢购 | Redis + Redisson | 性能极致,看门狗自动续期 |
| 金融交易、资金扣减 | ZooKeeper(Curator) | 强一致性,天然防死锁 |
| 低频后台任务 | 数据库锁 | 无需引入新组件,实现简单 |
| 复杂业务(兼顾性能与一致性) | 混合架构(Redis+数据库兜底) | 性能与一致性的最佳平衡 |
无论选择何种方案,都需要牢记分布式锁的四大核心设计准则:
-
互斥性:同一时刻仅一个客户端持有锁
-
安全性:持有锁的客户端宕机后锁能最终释放
-
容错性:锁服务大部分节点存活时即可正常工作
-
效率:加解锁性能不能成为系统瓶颈