分布式ID的实现方式:
- UUID
- Redis自增
- 数据库自增
- snowflake算法(雪花算法)
这里我们使用自定义的方式实现:时间戳+序列号+数据库自增
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息,比如时间戳、UUID、业务关键词
- 符号位:1bit,永远为0(表示正数)
- 时间戳:31bit,以秒为单位,可以使用69年(2^{31}/3600/24/365≈69)
- 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
业务流程:
单体下一人多单超卖问题
为什么会产生超卖问题呢?
超卖问题的常见解决方案:
-
悲观锁:
- 思想: 认为冲突必然发生,操作前先加锁,保证串行。
- 实现:
synchronized、Lock。 - 特点: 强一致,性能较低(加锁开销),并发度低,冲突直接阻塞。
-
乐观锁:
- 思想: 认为冲突不必然发生,操作不加锁,提交时判断数据是否被修改。未修改则更新;修改则异常/重试。
- 实现: 版本号法、CAS。
- 特点: 性能高(无锁),并发度高,需处理提交冲突(重试/异常)。
悲观锁 vs 乐观锁:
- 性能: 乐观锁 > 悲观锁(无加锁开销)。
- 冲突处理: 悲观锁阻塞线程;乐观锁重试/抛异常。
- 并发度: 乐观锁 > 悲观锁。
- 场景: 写多冲突多用悲观锁;读多冲突少用乐观锁。
CAS (乐观锁核心):
-
含义: Compare and Swap (比较并交换),原子操作。
-
参数: 内存地址
V,预期旧值A,新值B。 -
操作:
if (V == A) then V = B。原子执行。 -
本质: “我认为
V应该是A,如果是,我改成B;否则失败”。 -
结果: 成功返回 true;失败返回 false (值被改)。
-
应用: 更新时重试直到成功。
-
问题:
- ABA:
A->B->A变化无法感知。解决: 加版本号/标记位。 - 自旋开销: 竞争激烈时重试消耗CPU。
- 单一变量: 只能保证一个共享变量的原子操作。
- ABA:
乐观锁解决一人多单超卖问题
悲观锁解决超卖问题
乐观锁需要判断数据是否修改,而当前是判断当前是否存在,所以无法像解决库存超卖一样使用CAS机制,但是可以使用版本号法,但是版本号法需要新增一个字段,所以这里为了方便,就直接演示使用悲观锁解决超卖问题
集群下的一人一单超卖问题
一、synchronized为何在分布式环境中失效?
synchronized 是 Java 提供的本地锁机制,用于线程间同步,适用于同一个 JVM 实例内的多线程并发场景。它通过对象的监视器锁(monitor) 来控制访问,同一时间内只有一个线程可以持有锁,其它线程会被阻塞。
但在分布式系统中,不同的节点运行在不同的 JVM 实例中,每个实例的内存空间和锁监视器彼此独立,因此无法共享 synchronized 锁,从而导致锁在集群环境中失效。
二、分布式锁的必要性与核心特性
✅ 什么是分布式锁?
分布式锁是一种跨进程、跨节点的互斥机制,用于保证在分布式系统中某一时刻只有一个节点可以访问临界资源,从而防止并发冲突与数据不一致。
✅ 分布式锁的核心特性:
| 特性 | 说明 |
|---|---|
| 可见性 | 多节点之间锁状态需可见(共享锁状态) |
| 互斥性 | 同一时刻只能有一个节点获取到锁 |
| 高可用性 | 锁服务应容错(如节点宕机自动释放锁) |
| 可重入性 | 同一个节点可多次获取锁而不会死锁 |
| 自动过期 | 设置超时时间,避免死锁(例如宕机后不释放锁) |
| 高性能 | 尽量降低锁竞争开销,提高系统吞吐量 |
三、常见分布式锁实现方式
| 实现方式 | 原理简述 |
|---|---|
| 基于数据库 | 通过唯一索引 + 事务控制插入一条锁记录,冲突即认为锁被占用。适用于小规模系统,但性能和可靠性受限。 |
| 基于Redis | 使用 SET key value NX EX 实现原子加锁(推荐 Redis >= 2.6.12),利用 Redis 的高性能和原子指令保障锁行为,支持超时机制、可重试。 |
| 基于ZooKeeper | 创建临时有序节点,所有竞争者排序,最小节点获得锁。天然支持自动释放(会话断开自动删除节点),适合对一致性要求高的场景。 |
| 基于分布式算法 | 如 Google Chubby、Zookeeper、DLM 等协调器设计,通常用于企业级强一致系统,开发成本高但功能强大。 |
四、Redis 实现分布式锁的关键指令:SETNX
SETNX(SET if Not Exists)是 Redis 实现分布式锁的基础操作,其行为如下:
- 如果 key 不存在,则设置成功并返回
1; - 如果 key 已存在,设置失败并返回
0; - 通常配合过期时间(EX)与唯一标识(UUID)实现锁的安全与释放控制。
补充:现代 Redis 实现推荐使用 SET key value NX EX(或 PX)命令,集原子性、唯一性、超时机制于一体,具备更好的鲁棒性。
五、总结
synchronized只能在单 JVM 内使用,无法适配多节点并发控制;- 分布式锁是解决集群并发冲突的核心机制;
- Redis 是目前分布式锁实现中最主流、性能最优、实现简便的方案之一;
- 对于复杂协调或强一致场景,可以考虑 ZooKeeper 或 DLM 类方案。
分布式锁解决超卖问题
分布式锁优化
分布式锁优化1
本次优化主要解决了锁超时释放出现的超卖问题
解决办法:给分布式锁加线程标识,释放时判断是否自己的锁,是则释放、否则不释放,解决多线程同时获锁导致的超卖。
分布式锁优化2
本次优化主要解决了释放锁时的原子性问题。说到底也是锁超时释放的问题
问题:给锁加线程标识及释放判断,虽减少超卖但仍可能发生:线程 1 判锁后阻塞,锁超时释放,线程 2 获锁;线程 1 恢复后删除线程 2 的锁,致线程 3 进入,继续超卖。
解决方案:可用 Lua 脚本保障判断锁与释放锁的原子性。Redis 通过同一 Lua 解释器运行所有命令,确保脚本原子执行 —— 执行期间不运行其他脚本或 Redis 命令,类似 MULTI/EXEC,对其他客户端而言,脚本效果要么不可见要么已完成。需注意:单个 Lua 脚本执行时虽暂停其他操作以保原子性,但脚本自身出错则无法保障(其中 Redis 指令出错会回滚)。
释放锁的业务流程是这样的:
- 获取锁中的线程标示
- 判断是否与指定的标示(当前线程标示)一致
- 如果一致则释放锁(删除)
- 如果不一致则什么都不做
一、为何继续优化已有的分布式锁?
虽然我们基于 Redis 的 SETNX + EXPIRE 实现了基础的分布式锁,并通过设置超时时间、释放机制等方式达到了“生产可用”的程度,但在实际生产环境中,仍存在以下几个关键问题:
1. 不可重入
分布式锁不支持可重入性,即同一线程多次加锁会发生死锁。
例如:
public void methodA() {
lock();
methodB(); // methodB 内也调用了 lock()
unlock();
}
如果 methodA 和 methodB 中使用的是同一把分布式锁,而该锁不支持重入,就会导致线程在执行 methodB 时阻塞自己,最终形成死锁。
2. 不可重试
传统的 SETNX 实现只尝试一次加锁,如果失败就直接返回 false,这在高并发场景中容易导致锁竞争失败,甚至出现业务未执行即终止的情况,从而引发数据丢失或流程中断。
3. 锁超时释放问题
- 若设置的锁过期时间太短,可能出现业务尚未完成,锁却提前释放,导致多个线程并发执行业务逻辑,引发数据冲突;
- 若设置过期时间太长,一旦线程异常终止或网络异常,锁迟迟不释放,导致资源被长时间占用,产生 “伪死锁”。
✅ 解决思路:锁续期 + 心跳机制
在加锁成功后,开启一个后台定时任务(watchdog) ,定时刷新锁的过期时间(例如每10秒刷新一次,延长为30秒),从而保证锁在业务执行期间不会被提前释放。
4. 主从一致性问题
Redis 主从集群存在数据复制延迟,若客户端从主节点获取锁成功,但此时主节点宕机、从节点未同步成功,故障转移后锁记录丢失,导致多个客户端误以为未被加锁,从而造成锁失效和并发冲突。
二、推荐方案:使用成熟组件 Redisson
为避免重复造轮子并解决上述问题,推荐使用 Redisson —— 一个功能完善的 Redis 客户端工具,封装了大量高可用的分布式解决方案。
✅ Redisson 提供的分布式能力包括:
- 分布式锁(可重入、公平锁、读写锁等)
- 分布式同步器(信号量、闭锁等)
- 分布式集合/对象(Map、List、Set 等)
- 分布式服务(任务队列、限流器等)
三、Redisson 的 tryLock() 方法详解
Redisson 提供多种重载版本的 tryLock() 方法,便于根据业务需要灵活配置锁行为:
| 方法签名 | 描述 |
|---|---|
tryLock() | 无参版本,不等待立即返回,锁默认有效期为 30 秒。 |
tryLock(long waitTime, TimeUnit unit) | 在指定等待时间内尝试获取锁,超时未获取成功则返回 false。 |
tryLock(long waitTime, long leaseTime, TimeUnit unit) | 等待 waitTime 时间获取锁,锁的有效期为 leaseTime,到期自动释放。 |
🔍 默认行为说明:
waitTime = -1:表示不等待,立即尝试一次;leaseTime = 30:表示锁的持有时间为 30 秒;unit = TimeUnit.SECONDS:时间单位为秒。
📝 示例:
RLock lock = redissonClient.getLock("myLock");
// 尝试获取锁,最多等待10秒,成功后锁定时间为60秒
if (lock.tryLock(10, 60, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
}
四、总结
| 问题 | Redisson 解决方案 |
|---|---|
| 不可重入 | 提供可重入锁 |
| 无重试机制 | 支持等待超时机制 |
| 锁易失效 | 提供自动续期机制(watchdog) |
| 主从延迟 | 使用 RedLock 或部署单节点 Redis 避免主从问题 |
| 功能单一 | 提供丰富的分布式对象与同步工具 |
Redisson 是 Redis 分布式开发中的首选轮子,大大降低了业务复杂度,推荐在分布式系统中优先考虑使用。
可重入锁的原理
Redisson分布式锁原理:
-
如何解决可重入问题:利用hash结构记录线程id和重入次数。
-
如何解决可重试问题:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制。
-
如何解决超时续约问题:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间。
-
如何解决主从一致性问题:利用Redisson的multiLock,多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功。缺陷:运维成本高、实现复杂
分布式系统下的超卖问题与分布式锁演进历程
在系统开发过程中,我们从最初的单体系统逐步演进到分布式架构,也在实践中逐步构建并完善了分布式锁方案,以应对各种高并发下的超卖问题。
🧩 阶段一:自增ID冲突问题 —— 实现分布式ID
- 问题:在多线程环境下生成订单 ID 时,使用数据库自增字段或雪花算法存在冲突或瓶颈。
- 解决方案:实现分布式ID(如 Redis 计数器、雪花算法、Leaf、TinyId 等),保证高并发环境下的唯一性和高性能。
🧩 阶段二:一人多单导致超卖 —— 乐观锁
- 问题:在单体应用中,用户频繁提交订单,导致商品库存被多次扣减,出现一人多单导致超卖。
- 解决方案:使用乐观锁机制(如数据库版本号控制),通过更新时校验版本号来避免并发写冲突。
🧩 阶段三:改为一人一单,仍超卖 —— 悲观锁
- 问题:即便改为一人一单,在高并发场景下,用户多次快速请求仍可能导致超卖。
- 原因:并发请求几乎同时读取库存,导致多个线程判断都为“有库存”。
- 解决方案:使用悲观锁(如
synchronized或数据库锁)来同步处理订单请求,避免并发写入。
🧩 阶段四:系统升级为集群,锁失效 —— 分布式锁
- 问题:单体应用升级为集群部署后,
synchronized等本地锁失效(因锁只在一个 JVM 中可见),仍然出现并发下单超卖问题。 - 解决方案:引入分布式锁,将锁的状态存储在 Redis 等共享存储中,实现集群下的线程互斥访问。
🧩 阶段五:分布式锁超时释放 —— 加入线程标识
- 问题:业务执行时间超过锁的过期时间(业务执行到一半的时候被阻塞),锁被 Redis 自动释放,其他线程误抢锁,导致数据错误。
- 解决方案:为每把锁添加唯一线程标识,释放锁时校验是否为自己加的锁,避免误删他人锁。
🧩 阶段六:释放锁非原子操作 —— 使用 Lua 脚本
- 问题:判断锁归属和删除锁是两个操作,非原子性,可能出现误删他人锁导致并发问题。
- 解决方案:使用 Redis Lua 脚本将“判断锁归属 + 删除锁”封装为原子操作,确保锁释放的安全性。
🧩 阶段七:锁不可重入 —— 引入 Hash + Lua 实现可重入锁
-
问题:分布式锁默认不可重入,若一个线程在嵌套方法中多次加锁会产生死锁。
-
解决方案:
- 将锁以 Redis Hash 结构存储:
key=锁名,field=线程标识,value=重入次数。 - 每次加锁
value+1,释放锁value-1,当value=0时真正释放锁。 - 获取与释放操作均使用 Lua 脚本封装为原子操作,保障并发安全。
- 将锁以 Redis Hash 结构存储:
🧩 阶段终:直接使用成熟方案 —— Redisson
-
问题:我们自己实现的分布式锁虽然逐步完善,但仍存在开发复杂、维护成本高等问题。
-
解决方案:引入成熟的开源框架 Redisson,它完美封装了我们前面所有优化点:
- ✅ 可重入锁
- ✅ 公平锁
- ✅ 自动续期机制(watchdog)
- ✅ 支持锁的等待时间和过期时间
- ✅ 支持分布式读写锁、信号量、闭锁等
- ✅ 所有操作均保障原子性,内部基于 Lua 脚本实现
✅ 总结:技术演进路线图
分布式ID → 乐观锁 → 悲观锁 → 分布式锁 → 加线程标识 → Lua 原子操作 → 可重入锁 → Redisson
从最初的并发问题,到锁的细节优化,再到引入 Redisson,我们经历了一整套分布式并发控制的进阶过程,这不仅解决了实际问题,也沉淀出一套成熟的分布式锁解决思路。