分布式ID的实现与秒杀业务

75 阅读13分钟

分布式ID的实现方式:

  1. UUID
  2. Redis自增
  3. 数据库自增
  4. snowflake算法(雪花算法)

这里我们使用自定义的方式实现:时间戳+序列号+数据库自增

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息,比如时间戳、UUID、业务关键词

QQ_1754577211451.png

  • 符号位:1bit,永远为0(表示正数)
  • 时间戳:31bit,以秒为单位,可以使用69年(2^{31}/3600/24/365≈69)
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

业务流程

QQ_1754577398807.png

单体下一人多单超卖问题

为什么会产生超卖问题呢?

QQ_1754577492242.png

超卖问题的常见解决方案

  1. 悲观锁:

    • 思想:  认为冲突必然发生,操作前先加锁,保证串行。
    • 实现:  synchronizedLock
    • 特点:  强一致,性能较低(加锁开销),并发度低,冲突直接阻塞。
  2. 乐观锁:

    • 思想:  认为冲突不必然发生,操作不加锁,提交时判断数据是否被修改。未修改则更新;修改则异常/重试。
    • 实现:  版本号法、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。
    • 单一变量:  只能保证一个共享变量的原子操作。

乐观锁解决一人多单超卖问题

QQ_1754634763746.png

QQ_1754634927747.png

QQ_1754635873214.png

悲观锁解决超卖问题

乐观锁需要判断数据是否修改,而当前是判断当前是否存在,所以无法像解决库存超卖一样使用CAS机制,但是可以使用版本号法,但是版本号法需要新增一个字段,所以这里为了方便,就直接演示使用悲观锁解决超卖问题

QQ_1754636083244.png

集群下的一人一单超卖问题

一、synchronized为何在分布式环境中失效?

synchronized 是 Java 提供的本地锁机制,用于线程间同步,适用于同一个 JVM 实例内的多线程并发场景。它通过对象的监视器锁(monitor) 来控制访问,同一时间内只有一个线程可以持有锁,其它线程会被阻塞。

QQ_1754638862319.png

但在分布式系统中,不同的节点运行在不同的 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 类方案。

分布式锁解决超卖问题

QQ_1754638996604.png

分布式锁优化

分布式锁优化1

本次优化主要解决了锁超时释放出现的超卖问题

QQ_1754639437668.png

解决办法:给分布式锁加线程标识,释放时判断是否自己的锁,是则释放、否则不释放,解决多线程同时获锁导致的超卖。

QQ_1754639510424.png

分布式锁优化2

本次优化主要解决了释放锁时的原子性问题。说到底也是锁超时释放的问题

问题:给锁加线程标识及释放判断,虽减少超卖但仍可能发生:线程 1 判锁后阻塞,锁超时释放,线程 2 获锁;线程 1 恢复后删除线程 2 的锁,致线程 3 进入,继续超卖。

QQ_1754640338379.png

解决方案:可用 Lua 脚本保障判断锁与释放锁的原子性。Redis 通过同一 Lua 解释器运行所有命令,确保脚本原子执行 —— 执行期间不运行其他脚本或 Redis 命令,类似 MULTI/EXEC,对其他客户端而言,脚本效果要么不可见要么已完成。需注意:单个 Lua 脚本执行时虽暂停其他操作以保原子性,但脚本自身出错则无法保障(其中 Redis 指令出错会回滚)。

QQ_1754640690932.png

释放锁的业务流程是这样的:

  1. 获取锁中的线程标示
  2. 判断是否与指定的标示(当前线程标示)一致
  3. 如果一致则释放锁(删除)
  4. 如果不一致则什么都不做

QQ_1754640750087.png

一、为何继续优化已有的分布式锁?

虽然我们基于 Redis 的 SETNX + EXPIRE 实现了基础的分布式锁,并通过设置超时时间、释放机制等方式达到了“生产可用”的程度,但在实际生产环境中,仍存在以下几个关键问题:

1. 不可重入

分布式锁不支持可重入性,即同一线程多次加锁会发生死锁。

例如:

public void methodA() {
    lock();
    methodB(); // methodB 内也调用了 lock()
    unlock();
}

如果 methodAmethodB 中使用的是同一把分布式锁,而该锁不支持重入,就会导致线程在执行 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 分布式开发中的首选轮子,大大降低了业务复杂度,推荐在分布式系统中优先考虑使用。

可重入锁的原理

QQ_1754655302624.png

QQ_1754655635172.png

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 脚本封装为原子操作,保障并发安全。

🧩 阶段终:直接使用成熟方案 —— Redisson

  • 问题:我们自己实现的分布式锁虽然逐步完善,但仍存在开发复杂、维护成本高等问题。

  • 解决方案:引入成熟的开源框架 Redisson,它完美封装了我们前面所有优化点:

    • ✅ 可重入锁
    • ✅ 公平锁
    • ✅ 自动续期机制(watchdog)
    • ✅ 支持锁的等待时间和过期时间
    • ✅ 支持分布式读写锁、信号量、闭锁等
    • ✅ 所有操作均保障原子性,内部基于 Lua 脚本实现

✅ 总结:技术演进路线图

分布式ID → 乐观锁 → 悲观锁 → 分布式锁 → 加线程标识 → Lua 原子操作 → 可重入锁 → Redisson

从最初的并发问题,到锁的细节优化,再到引入 Redisson,我们经历了一整套分布式并发控制的进阶过程,这不仅解决了实际问题,也沉淀出一套成熟的分布式锁解决思路。