一、背景说明:为什么要做秒杀优化?

在高并发秒杀场景中,系统主要面临两个核心问题:

  • 超卖问题:库存被并发扣减,出现负数。
  • 一人一单问题:同一用户在极短时间内多次下单成功。

黑马点评项目围绕这两个问题,逐步引出了多种并发控制方案,非常适合作为后端学习Redis与高并发的入门实战

二、第一阶段:数据库层 —— 乐观锁解决超卖问题

在最初的实现中,库存扣减直接依赖数据库:

UPDATE voucher SET stock = stock - 1 WHERE id = ? AND stock > 0;

或者配合版本号(version)的方式实现乐观锁

优点:

  • 实现简单
  • 不引入额外中间件

问题:

  • 高并发下数据库压力极大
  • 只能解决超卖,无法解决一人一单
  • 频繁失败重试,性能差

➡️ 结论:数据库并不是并发控制的理想位置。

三、第二阶段:单体环境 —— 悲观锁解决一人一单

在单体部署环境下,可以通过synchronized或ReentrantLock实现线程隔离:

synchronized (userId.toString().intern()) {
// 判断是否下过单
// 扣库存
// 创建订单
}

优点:

  • 能保证单JVM内的一人一单
  • 逻辑直观

问题:

  • 锁只在JVM内生效
  • 多实例部署时彻底失效

➡️ 引出分布式锁的必要性

四、第三阶段:Redis分布式锁 —— SETNX解决跨JVM并发问题

黑马点评使用Redis的SETNX(set if not exists)实现分布式锁

SET lock:order:userId value NX EX 10

作用:

  • 跨JVM保证一人一单
  • Redis性能远高于数据库

五、分布式锁的隐藏问题与解决方案

1️⃣ 锁误删问题

如果锁过期后被其他线程获取,原线程再执行DEL,会误删别人的锁。

解决方案:

  • 锁value = UUID + 线程ID
  • 删除时先校验是否为当前线程持有

2️⃣ 校验 + 删除的原子性问题

"判断锁是谁 + 删除锁"不是原子操作。

解决方案:Lua脚本

if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0

➡️ 保证Redis侧原子性执行

六、SETNX的局限性 & Redisson的引入

原生setnx实现的分布式锁仍存在问题:

  • ❌ 不可重入
  • ❌ 不支持阻塞重试
  • ❌ 锁续期需要自行实现

黑马点评后续通过Redisson提供的分布式锁解决这些问题:

  • 可重入锁
  • 看门狗自动续约
  • 支持公平锁、读写锁等

➡️ 更接近生产级分布式锁方案。

七、最终优化:引入消息队列进行秒杀削峰

在Redis校验通过后,不直接创建订单,而是:

  1. Redis中完成资格校验与库存预扣
  2. 将下单请求写入消息队列
  3. 由异步消费者完成订单落库

优势:

  • 削峰填谷
  • 解耦秒杀入口与下单逻辑
  • 提升系统整体吞吐能力

八、总结:黑马点评秒杀的核心设计思路

如果用一句话来总结黑马点评秒杀的演进路线的话,我觉得可以这样表述:

从数据库乐观锁防超卖 → JVM悲观锁防并发 → Redis分布式锁解决一人一单 → Redisson提供生产级锁能力 → 消息队列完成最终秒杀优化

Logo

更多推荐