秒杀系统的核心目标就一句话把高并发请求挡在数据库外面用 Redis 做快速库存校验用 MQ 异步削峰最终用数据库保证订单和库存的最终一致性。1. 秒杀系统整体架构可以设计成这样用户 ↓ 前端页面 / CDN 静态资源缓存 ↓ 网关 Gateway / Nginx ↓ 限流、黑名单、防刷、验证码 ↓ 秒杀服务 ↓ Redis库存预扣、用户去重、活动状态 ↓ MQ异步下单削峰 ↓ 订单服务 ↓ MySQL订单表、库存表、支付表 ↓ 支付服务核心思想是能在前端挡住的不进后端 能在网关挡住的不进业务服务 能在 Redis 解决的不查数据库 能异步处理的不同步阻塞用户。2. 秒杀前活动预热秒杀不能等用户来了再查数据库应该提前把数据加载到 Redis。比如活动开始前 5 分钟把这些信息放入 Redisseckill:stock:1001 500 seckill:goods:1001 商品信息 seckill:start:1001 开始时间 seckill:end:1001 结束时间库存表中也保留真实库存seckill_goods ------------- id goods_id stock start_time end_time statusRedis 用来抗并发MySQL 用来最终落库。3. 秒杀中请求流程用户点击秒杀后不应该直接创建订单而是走这个流程1. 判断活动是否开始 2. 判断用户是否重复购买 3. 判断库存是否充足 4. Redis 原子扣减库存 5. 发送 MQ 消息 6. 立即返回“排队中” 7. 订单服务异步消费 MQ 创建订单 8. 用户轮询订单结果接口可以这样设计POST /seckill/{goodsId}返回结果不是订单详情而是{ code: 0, message: 秒杀请求已提交排队中, requestId: xxx }然后前端轮询GET /seckill/result/{requestId}4. Redis 原子扣库存防止超卖秒杀最怕的问题是库存只有 1 个同时来了 100 个请求结果卖出 100 个。所以不能这样写Integer stock redis.get(stock); if (stock 0) { redis.decr(stock); }因为get和decr不是原子操作。应该用Lua 脚本让判断库存、判断重复购买、扣库存一次性完成。示例 Lualocal stockKey KEYS[1] local userKey KEYS[2] local userId ARGV[1] if redis.call(sismember, userKey, userId) 1 then return 2 end local stock tonumber(redis.call(get, stockKey)) if stock nil or stock 0 then return 0 end redis.call(decr, stockKey) redis.call(sadd, userKey, userId) return 1返回值含义0库存不足 1扣减成功 2重复购买Redis key 可以设计成seckill:stock:goodsId seckill:users:goodsId例如seckill:stock:1001 seckill:users:10015. 为什么不用分布式锁很多人一想到秒杀就用分布式锁例如 Redisson 锁。但是高并发秒杀场景下不推荐每个请求都抢锁。因为锁会让请求串行化10000 个请求排队抢锁性能会很差。更推荐Redis Lua 原子扣库存 MQ 异步下单分布式锁可以用于活动初始化、库存回补、后台管理等低频场景不适合所有秒杀请求都加锁。6. MQ 异步下单削峰秒杀请求成功预扣库存后不应该马上写数据库而是发送 MQ{ requestId: xxx, userId: 10001, goodsId: 1001, activityId: 2001 }订单服务消费 MQ 后创建订单1. 判断订单是否已存在 2. 扣减数据库库存 3. 创建订单 4. 写入支付状态 5. 更新秒杀结果数据库层面也要防止重复下单订单表可以加唯一索引CREATE UNIQUE INDEX uk_user_activity ON seckill_order(user_id, activity_id);这样即使 MQ 重复消费也不会重复生成订单。7. 数据库库存扣减也要防超卖虽然 Redis 已经预扣库存但数据库还是要兜底。推荐 SQLUPDATE seckill_goods SET stock stock - 1 WHERE id #{goodsId} AND stock 0;然后判断影响行数影响行数 1扣减成功 影响行数 0库存不足不要先查再改SELECT stock FROM seckill_goods WHERE id ? UPDATE seckill_goods SET stock stock - 1 WHERE id ?这样并发下容易出问题。8. 用户重复下单怎么防要做三层防重。第一层Redis 防重seckill:users:goodsId用户成功抢购后把 userId 放进 set。第二层订单表唯一索引UNIQUE(user_id, activity_id)第三层接口幂等每次秒杀请求生成一个 requestIdseckill:result:requestId防止用户重复点击、浏览器重试、网关重试导致重复下单。9. 秒杀结果怎么返回不要让用户一直卡在接口里等订单创建完成。秒杀接口只返回排队中然后订单服务处理完 MQ 后把结果写 Redisseckill:result:requestId SUCCESS:orderId seckill:result:requestId FAIL:库存不足 seckill:result:requestId FAIL:重复购买前端轮询结果每 1 秒查一次最多查 10 次。也可以用 WebSocket但普通项目里轮询就够了。10. 支付超时怎么处理秒杀订单不能一直占库存。订单创建后状态是待支付如果 15 分钟内没有支付需要自动关闭订单并回补库存。可以用MQ 延迟消息流程1. 创建订单成功 2. 发送一条 15 分钟后的延迟消息 3. 15 分钟后检查订单状态 4. 如果还是未支付关闭订单 5. 回补库存订单状态可以设计成0 待支付 1 已支付 2 已取消 3 已超时关闭 4 已退款支付成功回调时要注意幂等如果订单已经支付不重复处理 如果订单已关闭要走退款或人工处理 如果订单待支付更新为已支付。11. MQ 消息丢失怎么办这是秒杀系统的重点。如果 Redis 扣库存成功但是 MQ 发送失败就会出现Redis 库存少了但是订单没创建。解决方案方案一本地消息表Redis 扣减成功后写一条本地消息表然后再发 MQ。seckill_message --------------- id request_id user_id goods_id status retry_count create_time后台定时任务扫描未发送成功的消息重试发送 MQ。方案二事务消息如果用 RocketMQ可以使用事务消息。流程1. 发送半消息 2. 执行业务 3. 提交消息 4. MQ 回查事务状态普通项目用本地消息表更容易实现。12. MQ 重复消费怎么办MQ 至少一次投递所以订单服务必须支持幂等。处理方式1. requestId 唯一 2. userId activityId 唯一索引 3. 消费记录表例如CREATE TABLE mq_consume_log ( id BIGINT PRIMARY KEY, message_id VARCHAR(64) UNIQUE, status TINYINT, create_time DATETIME );消费前先判断消息是否处理过。13. Redis 崩了怎么办可以做几件事1. Redis Cluster 集群 2. 活动库存提前预热 3. Redis 开启 AOF 4. 秒杀接口降级 5. 库存数据定时同步如果 Redis 不可用不建议直接打到数据库可以直接返回系统繁忙请稍后重试因为 Redis 挂了之后让请求直接进 MySQL数据库很可能也会被打挂。14. 限流和防刷设计秒杀一定要限流否则再好的库存设计也扛不住。可以做这些限制网关限流例如同一个 IP 每秒最多 10 次 同一个用户每秒最多 3 次 同一个活动接口整体 QPS 限制验证码秒杀开始前先让用户获取验证码降低机器请求。秒杀令牌不要直接暴露真实秒杀接口。流程1. 用户先请求 /seckill/token 2. 后端判断用户资格 3. 返回一次性 token 4. 用户带 token 请求 /seckill 5. token 校验通过后才能进入秒杀逻辑Redis 存seckill:token:userId:goodsId token使用后立即删除防止重复提交。接口隐藏秒杀真实地址动态生成/seckill/do/{path}path 放 Redis有效期 60 秒。15. 库存一致性怎么保证秒杀库存有三份Redis 库存 数据库库存 订单库存设计原则Redis 负责抗并发 数据库负责最终准确 订单状态负责业务闭环。可能出现的问题和处理方式问题解决方案Redis 扣了MQ 失败本地消息表或事务消息MQ 成功订单创建失败消费失败重试最终失败则回补 Redis用户不支付延迟消息关闭订单回补库存支付回调重复支付回调幂等处理数据不一致定时对账任务16. 订单表设计示例CREATE TABLE seckill_order ( id BIGINT PRIMARY KEY, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, activity_id BIGINT NOT NULL, status TINYINT NOT NULL, pay_time DATETIME, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_order_no(order_no), UNIQUE KEY uk_user_activity(user_id, activity_id) );库存表CREATE TABLE seckill_goods ( id BIGINT PRIMARY KEY, goods_id BIGINT NOT NULL, activity_id BIGINT NOT NULL, stock INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL, create_time DATETIME, update_time DATETIME );17. 秒杀接口伪代码public SeckillResult seckill(Long userId, Long goodsId) { // 1. 判断活动时间 SeckillGoods goods getGoodsFromRedis(goodsId); if (goods null) { return fail(活动不存在); } if (nowBeforeStart(goods)) { return fail(活动未开始); } if (nowAfterEnd(goods)) { return fail(活动已结束); } // 2. 执行 Lua 脚本判断重复购买 判断库存 扣库存 Long result redis.execute(luaScript, stockKey, userKey, userId); if (result 0) { return fail(库存不足); } if (result 2) { return fail(不能重复购买); } // 3. 生成请求 ID String requestId IdWorker.nextId(); // 4. 发送 MQ SeckillMessage message new SeckillMessage(); message.setRequestId(requestId); message.setUserId(userId); message.setGoodsId(goodsId); mq.send(message); // 5. 返回排队中 return success(排队中, requestId); }18. MQ 消费创建订单伪代码RabbitListener(queues seckill.order.queue) public void createOrder(SeckillMessage message) { String requestId message.getRequestId(); Long userId message.getUserId(); Long goodsId message.getGoodsId(); // 1. 幂等判断 if (orderExists(userId, goodsId)) { saveResult(requestId, REPEAT); return; } // 2. 扣数据库库存 int count seckillGoodsMapper.decreaseStock(goodsId); if (count 0) { // 数据库库存不足回补 Redis redis.incr(seckill:stock: goodsId); redis.srem(seckill:users: goodsId, userId); saveResult(requestId, FAIL); return; } // 3. 创建订单 Order order new Order(); order.setUserId(userId); order.setGoodsId(goodsId); order.setStatus(0); orderMapper.insert(order); // 4. 发送延迟消息用于超时取消订单 delayMq.send(order.getId(), 15, TimeUnit.MINUTES); // 5. 保存秒杀结果 saveResult(requestId, SUCCESS: order.getId()); }19. 面试时可以这样总结秒杀系统我会从四个方面设计第一前端和网关层限流避免无效流量进入系统。 第二活动开始前将商品和库存预热到 Redis秒杀时使用 Lua 脚本完成用户去重和库存预扣保证原子性防止超卖。 第三Redis 扣减成功后发送 MQ订单服务异步消费消息创建订单用 MQ 削峰填谷避免高并发直接打到数据库。 第四数据库层通过 stock 0 条件更新和唯一索引兜底同时配合支付超时关闭、库存回补、MQ 幂等消费和定时对账保证最终一致性。更简洁一点秒杀系统的关键是限流、防刷、Redis 原子扣库存、MQ 异步下单、数据库兜底、防重复下单、支付超时回补和数据对账。20. 一个比较完整的秒杀流程图用户点击秒杀 ↓ 前端限流 / 按钮置灰 ↓ 网关限流 / 黑名单 / IP 限制 ↓ 校验秒杀 token ↓ 判断活动时间 ↓ Redis Lua - 判断是否重复购买 - 判断库存是否充足 - 扣减库存 ↓ 扣减成功 ↓ 发送 MQ ↓ 返回排队中 ↓ 订单服务消费 MQ ↓ 数据库扣库存 ↓ 创建订单 ↓ 返回秒杀成功 ↓ 用户支付 ↓ 支付成功订单完成 ↓ 超时未支付关闭订单回补库存你可以把秒杀系统记成一句口诀前端限流网关拦截 Redis 预扣Lua 原子 MQ 削峰异步下单 MySQL 兜底唯一索引 支付超时库存回补 消息幂等定时对账。