设计秒杀系统,核心就三个字:快、稳、准。
咱们直接看一个全局架构图,心里先有个底 👇


这个链路,就是我们今天要手撕的全部。
挑战 | 本质问题 | 后果 |
|---|---|---|
瞬时高并发 | 短时间内流量暴增 100-1000 倍 | 服务器雪崩、数据库宕机 |
超卖问题 | 并发读写导致库存不一致 | 订单量 |
恶意请求 | 脚本刷单、黄牛抢购 | 普通用户抢不到,体验差 |
秒杀系统的核心思想是:分层限流 + 异步削峰 + 最终一致性,把流量层层拦截,最终只有极少部分请求能到达数据库。

我用一个时序图,把一次正确的秒杀请求走通:

/getSeckillPath),这个 URL 还可以做成一次性的、加盐动态生成的。秒杀最怕的就是海量请求直接把服务器打崩。所以,要将 99% 的不必要流量,拦截在上游。
超卖的根本原因是:多个线程同时读取到相同的库存值,然后各自扣减,导致最终库存为负。
方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
数据库行锁 |
| 简单可靠 | 数据库压力大 | 低并发秒杀 |
悲观锁 |
| 绝对不会超卖 | 性能极差,死锁风险 | 几乎不用 |
乐观锁 | 版本号机制 | 性能较好 | 高并发下成功率低 | 中低并发 |
Redis+Lua | 原子脚本扣减库存 | 性能极高,原子性 | 依赖 Redis 可靠性 | 高并发秒杀 ✅ |
分布式锁 | Redisson 分布式锁 | 通用性强 | 性能一般,复杂度高 | 复杂业务场景 |
面试官插话:你说了那么多,如果流量真的冲进来了,超卖问题到底怎么解决?
我: 这正是秒杀的灵魂所在。我们绝对不能在数据库层面直接排队扣减,那样 MySQL 会直接死给你看。
核心方案:Redis 预减库存 + 数据库最终一致性扣减
这是目前互联网大厂最主流的解决方案,性能最高且绝对不会超卖。
我们把秒杀库存提前预热到 Redis 里。关键是,扣减操作必须原子化。我们用的不是多条 Redis 命令,而是一段 Lua 脚本:
-- 原子扣减库存脚本
local stockKey = KEYS[1]
local userKey = KEYS[2]
local userId = ARGV[1]
-- 1. 判断用户是否已经抢购过
if redis.call('sismember', userKey, userId) == 1 then
return -1 -- 已抢购
end
-- 2. 判断库存是否充足
local stock = tonumber(redis.call('get', stockKey))
if stock <= 0 then
return 0 -- 库存不足
end
-- 3. 扣减库存并标记用户已抢购
redis.call('decr', stockKey)
redis.call('sadd', userKey, userId)
return 1 -- 抢购成功为什么这样能防止超卖? 因为 Redis 是单线程执行命令的,Lua 脚本在执行时会把整个脚本当成一个原子操作。绝对不会出现“读到还有库存,但去扣的时候被别人扣光了”的读后写竞态条件。🛡️
快返回、异步下单:MQ 削峰 📬
Redis 扣减成功后,通过 MQ 异步创建订单。如果订单创建失败,需要进行库存回补:
面试官追问:那如果消息队列出错了,或者 Redis 有什么问题,数据库层面怎么兜底?
我: 问得好。MySQL 是最后一道闸门,我们必须做最坏的打算。
消费服务慢慢从 MQ 拉消息,真正去创建订单、扣减数据库库存时,必须用乐观锁。
-- 不用版本号,直接用库存数当条件,更简洁
UPDATE seckill_product
SET stock = stock - 1
WHERE id = #{productId}
AND stock >= 1; -- 核心兜底条件如果是不限量的茅台,谁也救不了,只能用更大的集群硬扛。但我们可以:
AtomicInteger 自旋扣减,再定时异步同步给 Redis。这样连 Redis 的网络开销都省了。💨同学,我们回顾下这场秒杀设计的整个逻辑闭环,一张图记牢:
层次 | 核心组件/策略 | 解决的核心问题 |
|---|---|---|
👆 前端/网关 | CDN、验证码、动态URL、网关令牌桶 | 挡住垃圾流量,削减尖峰 |
⚡ 核心业务 | Redis + Lua 原子扣减 | 彻底杜绝超卖 |
🔄 异步处理 | 消息队列 RocketMQ/Kafka | 削峰填谷,将并发变流式 |
🗄️ 最终兜底 | 数据库乐观锁(库存 ≥ 1) | 防止极端情况下数据不一致 |
📈 可观测性 | 监控、告警、限流动态调整 | 随时知道系统还能撑多久 |
高并发秒杀系统的设计精髓就是:"挡"、"削"、"异" 三个字
超卖问题的最优解是Redis+Lua 原子脚本,它在保证原子性的同时,提供了极高的性能,完全能扛住百万级 QPS 的秒杀场景。
技术亮点:用 Lua 脚本将 "库存判断 + 扣减 + 用户限购" 打包成原子操作,彻底解决 Redis 并发下的竞态问题,比多次 Redis 调用性能高 3 倍以上。
/**
* 原子性预扣减库存(Lua脚本实现)
* @param goodsId 商品ID
* @param userId 用户ID
* @param limitCount 每人限购数量
* @return 0-成功 1-库存不足 2-已达限购上限
*/
public int deductStock(Long goodsId, Long userId, int limitCount) {
String luaScript = """
local stockKey = KEYS[1]
local userKey = KEYS[2]
local limit = tonumber(ARGV[1])
-- 1. 判断用户是否已达限购上限
local userBuyCount = redis.call('HGET', userKey, ARGV[2])
if userBuyCount and tonumber(userBuyCount) >= limit then
return 2
end
-- 2. 判断库存是否充足
local stock = redis.call('GET', stockKey)
if not stock or tonumber(stock) <= 0 then
return 1
end
-- 3. 扣减库存+记录用户购买数量
redis.call('DECR', stockKey)
redis.call('HINCRBY', userKey, ARGV[2], 1)
return 0
""";
List<String> keys = Arrays.asList(
"seckill:stock:" + goodsId,
"seckill:user:" + goodsId
);
return (Integer) redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Integer.class),
keys,
limitCount,
userId.toString()
);
}技术亮点:解决原生 SETNX 锁的 "锁过期但业务未执行完" 问题,Redisson 看门狗自动续期,支持可重入,是生产环境唯一推荐的分布式锁实现。
/**
* 防止同一用户重复下单
*/
public Result seckill(Long goodsId, Long userId) {
String lockKey = "seckill:lock:" + goodsId + ":" + userId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待0秒,锁默认30秒过期(看门狗自动续期)
if (!lock.tryLock(0, TimeUnit.SECONDS)) {
return Result.error("请勿重复提交请求");
}
// 执行库存预扣减
int result = deductStock(goodsId, userId, 1);
if (result == 1) {
return Result.error("商品已售罄");
}
if (result == 2) {
return Result.error("您已参与过本次秒杀");
}
// 生成订单ID,发送消息到Kafka
String orderId = UUID.randomUUID().toString();
kafkaTemplate.send("seckill-order-topic",
new SeckillMessage(orderId, goodsId, userId));
return Result.success("排队成功,请等待订单生成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.error("系统繁忙,请稍后再试");
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}技术亮点:用订单 ID 作为唯一标识实现幂等性,解决 Kafka 消息重复消费导致的重复下单问题,同时配合事务保证消息处理的原子性。
/**
* Kafka秒杀订单消费者(幂等性实现)
*/
@KafkaListener(topics = "seckill-order-topic", groupId = "seckill-group")
public void onMessage(ConsumerRecord<String, String> record) {
SeckillMessage message = JSON.parseObject(record.value(), SeckillMessage.class);
String orderId = message.getOrderId();
// 1. 幂等性判断:如果订单已存在,直接返回
if (orderService.existOrder(orderId)) {
log.info("订单已存在,重复消费,orderId: {}", orderId);
return;
}
// 2. 事务处理:创建订单+扣减数据库库存
try {
orderService.createOrderWithStockDeduct(message);
} catch (Exception e) {
log.error("订单创建失败,orderId: {}", orderId, e);
// 发送到死信队列,人工处理
kafkaTemplate.send("seckill-dlq-topic", record.value());
}
}技术亮点:用 MySQL 行锁防止超卖,同时用乐观锁版本号控制并发更新,在保证数据一致性的前提下最大化性能。
-- 最终库存扣减(行锁实现,绝对不会超卖)
UPDATE seckill_goods
SET stock_count = stock_count - 1,
version = version + 1
WHERE goods_id = #{goodsId}
AND stock_count > 0
AND version = #{version};@RestController
public class SeckillController {
@Autowired
private SeckillStockService stockService;
@Autowired
private RocketMQTemplate mqTemplate;
@Autowired
private StringRedisTemplate redis;
// 秒杀接口
@PostMapping("/seckill/exec")
public Result exec(@RequestParam String itemId,
@RequestParam String captcha,
HttpServletRequest request) {
String uid = getLoginUserId(request);
// 1. 验证码校验(防脚本)
if (!checkCaptcha(uid, captcha)) {
return Result.fail("验证码错误");
}
// 2. 用户级限流:每人只允许一次请求
String limitKey = "seckill:limit:" + itemId + ":" + uid;
Boolean limited = redis.opsForValue()
.setIfAbsent(limitKey, "1", Duration.ofMinutes(1));
if (Boolean.FALSE.equals(limited)) {
return Result.fail("请勿重复提交");
}
// 3. 库存预扣
boolean deducted = stockService.deductStock(itemId, 10); // 10个分片
if (!deducted) {
return Result.fail("已抢光");
}
// 4. 发送 MQ 消息,异步下单
SeckillMessage msg = new SeckillMessage(uid, itemId);
mqTemplate.asyncSend("seckill_order", msg, new SendCallback() {
@Override public void onSuccess(SendResult result) {}
@Override public void onException(Throwable e) {
// 发送失败,补偿恢复库存 + 删除 limitKey
// ... 异常处理逻辑,可发一条补偿消息
}
});
// 5. 立即返回排队结果,前端轮询查结果
return Result.ok("排队中,请稍候查看结果");
}
// 查询秒杀结果
@GetMapping("/seckill/result")
public Result queryResult(@RequestParam String itemId, HttpServletRequest req) {
String uid = getLoginUserId(req);
String resultKey = "seckill:result:" + itemId + ":" + uid;
String result = redis.opsForValue().get(resultKey);
if (result == null) {
return Result.ok("排队中");
}
return Result.ok(result); // "success" 或 "fail"
}
}技术亮点:
技术难点 | 问题本质 | 解决方案 | 技术亮点 |
|---|---|---|---|
库存超卖问题 ⚠️ | 并发读写导致数据不一致,多个线程同时读到相同库存 | Redis Lua 脚本原子预扣 2. MySQL 行锁最终扣减 3. 库存回补机制 | 双层防护,Redis 挡 99% 流量,DB 做最终兜底,绝对不会超卖 |
分布式锁可靠性问题 🔓 | 原生 SETNX 锁过期但业务未执行完,导致多个线程同时拿到锁 | Redisson 可重入锁 2. 看门狗自动续期 3. 锁释放 finally 块 | 解决了锁过期、死锁、不可重入三大问题,生产级可靠 |
消息丢失与重复消费 📨 | Kafka 消息在生产、传输、消费过程中可能丢失或重复 | 生产者 ack=all + 重试 2. 消费者手动提交 offset 3. 订单 ID 幂等性 | 保证消息至少被消费一次,且不会重复处理 |
库存与订单一致性问题 🧩 | 库存扣减成功但订单创建失败,导致库存丢失 | 最终一致性模型 2. 定时任务补偿 3. 死信队列人工处理 | 用异步 + 补偿代替强事务,性能提升 10 倍以上 |
热点商品性能瓶颈 🔥 | 单个商品百万级并发请求,导致 Redis 单节点 CPU 打满 | 热点商品单独隔离 2. Redis 库存分片 3. 本地缓存预热 | 将热点流量分散到多个节点,单节点 QPS 从 10 万提升到 100 万 |
恶意请求与刷单问题 🤖 | 脚本机器人批量抢购,普通用户抢不到 | 多层验证码(滑块 / 点选) 2. 设备指纹识别 3. 风控系统实时拦截 | 拦截 99% 以上的恶意请求,保证活动公平性 |
系统高可用问题 🚨 | 秒杀流量突增导致服务雪崩 | Sentinel 限流降级 2. 服务集群多活部署 3. 全链路压测提前验证 | 系统过载时自动保护核心功能,不会整体宕机 |
订单超时取消问题 ⏰ | 用户下单后未支付,需要自动取消并回补库存 | Redisson 延迟队列 2. 定时任务兜底扫描 | 延迟队列精确到秒级,比定时任务性能高 100 倍 |

核心思路:
这样一套组合拳,既保证了高并发下的性能,又用最终一致性保证不超卖,大厂里就是这么玩的 😎。
【别走,交个朋友】
我是Rain ,一个喜欢把复杂技术讲透、让代码落地的实践者。
如果你厌倦了四处收集碎片化的八股文,想看看一线项目里真实的架构决策和踩坑记录,欢迎来我的公众号「Rain的Java大神之路」坐坐。
在这里,我会把每一次技术复盘、每一个项目的设计源码,毫无保留地分享给你。
关注后回复「源码」,我为你准备了几份我亲手注释的高并发项目核心源码,希望能帮你少走一些弯路。
🔥技术这条路很酷,我们结伴同行。
🚀Keep Coding, Keep Loving —— 与所有 Java 同路人共勉。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。