秒杀系统的架构演进从单体限流到 Redis MQ 的分布式秒杀方案一、从一次线上事故说起去年双十一我们团队负责的电商平台遭遇了一场典型的秒杀事故。运营部门策划了一场整点 1 元抢 iPhone活动预计并发 5000实际瞬时涌入 3.2 万并发请求。结果 MySQL 连接池被打满服务雪崩连正常下单都受到影响最终以紧急关闭活动收场。复盘时发现几个触目惊心的数据同一用户重复提交了 127 次请求库存扣减触发了 1.4 万次数据库行锁竞争超过 60% 的请求是无效请求库存已耗尽后的无效查询。这些问题在单体架构下几乎无法根治。二、流量治理层多层限流策略流量治理的第一道防线设在 Nginx 层通过 OpenResty Lua 实现连接级限流。我们部署了一个限流模块基于令牌桶 漏桶混合算法对秒杀接口实施独立的限流策略。限制单 IP 每秒最多 10 个请求全局 QPS 硬上限为 10000。超过硬上限的请求直接返回 HTTP 429不再穿透到后端服务。第二道防线是业务级防刷。我们在秒杀入口前增加了滑块验证码和秒杀问答如13?两者联合校验。验证通过后签发一个时效 60 秒的 Token后续请求必须携带该 Token。这个设计将机器刷单流量过滤掉约 95%。第三道防线在应用层通过 Guava RateLimiter 做细粒度的用户级和接口级控制。/** * 秒杀限流拦截器——多层流量控制 */ Component public class SeckillRateLimiter { // 全局令牌桶每秒10000个请求 private final RateLimiter globalLimiter RateLimiter.create(10000.0); // 用户级令牌桶缓存每秒5个请求 private final CacheString, RateLimiter userLimiterCache Caffeine.newBuilder() .expireAfterAccess(10, TimeUnit.MINUTES) .maximumSize(100_000) .build(); /** * 判断请求是否允许通过 */ public RateLimitResult tryAcquire(String userId, String ip) { // 第一层全局限流 if (!globalLimiter.tryAcquire(50, TimeUnit.MILLISECONDS)) { return RateLimitResult.reject(系统繁忙请稍后再试); } // 第二层用户级限流 RateLimiter userLimiter userLimiterCache.get(userId, key - RateLimiter.create(5.0)); if (!userLimiter.tryAcquire()) { log.warn(用户{}请求过于频繁已被限流, userId); return RateLimitResult.reject(操作过于频繁请稍后再试); } return RateLimitResult.pass(); } }三、库存扣减方案Redis 预扣 异步持久化库存扣减是秒杀系统的核心博弈点。如果直接在数据库层面执行UPDATE stock SET count count - 1 WHERE count 0在高并发下会因为行锁竞争导致严重性能瓶颈。我们的方案是将库存预先加载到 Redis利用 Redis 的单线程模型和 Lua 脚本实现原子性的库存预扣。具体流程是活动开始前 30 分钟将商品库存同步至 Redis 的 String 类型键。秒杀请求到达时执行 Lua 脚本完成检查库存 → 扣减库存 → 生成预订单号三步操作全程原子化。成功扣减后将预订单信息异步投递到 RabbitMQ由消费者线程完成最终的数据库持久化和后续的业务流程如生成正式订单、发送通知等。这个方案的精妙之处在于异步解耦用户侧的响应延迟只依赖 Redis 操作约 1~3ms而数据库写入、订单校验等耗时操作全部异步化。即使消费者处理速度跟不上MQ 也能提供缓冲能力保证 Redis 预扣的数据不会丢失。/** * Redis库存预扣——Lua脚本保证原子性 */ Component public class RedisStockDeduction { private static final String STOCK_DEDUCT_LUA local stock redis.call(get, KEYS[1]) if not stock or tonumber(stock) 0 then return -1 // 库存不足 end redis.call(decr, KEYS[1]) local orderId ARGV[1] redis.call(hset, KEYS[2], orderId, ARGV[2]) // 记录预订单 return tonumber(stock) - 1; Resource private StringRedisTemplate redisTemplate; /** * 预扣库存返回剩余库存数量-1表示库存不足 */ public long deductStock(String productId, String orderId, String userId) { String stockKey seckill:stock: productId; String orderKey seckill:orders: productId; // 构造订单详情JSON String orderDetail buildOrderDetail(orderId, userId, productId); DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(STOCK_DEDUCT_LUA); script.setResultType(Long.class); Long remaining redisTemplate.execute( script, Arrays.asList(stockKey, orderKey), orderId, orderDetail ); if (remaining ! null remaining 0) { log.info(商品{}库存已耗尽订单{}预扣失败, productId, orderId); throw new StockExhaustedException(商品已售罄); } log.info(商品{}库存预扣成功剩余{}订单{}, productId, remaining, orderId); return remaining ! null ? remaining : -1; } }四、异步下单与幂等保障RabbitMQ 消费者在处理预订单时面临的最大挑战是幂等性保障。消费者可能因网络抖动、重启等原因重复消费同一条消息如果重复执行持久化操作会导致库存超卖或重复订单。我们的方案分两层保障在消息层生产者发送消息时携带业务唯一 ID即预订单号RabbitMQ 配置手动 ACK 模式。消费者处理完成后发送 ACK如果处理过程中宕机消息会重回队列被重新消费。在数据库层订单表上设置预订单号pre_order_id为唯一索引即使重复消费第二次的INSERT操作也会因为唯一约束冲突而失败。同时使用乐观锁版本号控制库存表的并发更新。/** * MQ消费者——异步持久化订单含幂等保障 */ Component public class SeckillOrderConsumer { Resource private JdbcTemplate jdbcTemplate; RabbitListener(queues seckill.order.queue) public void handle(Message message, Channel channel) { String orderId null; try { String body new String(message.getBody(), StandardCharsets.UTF_8); OrderMessage orderMsg JSON.parseObject(body, OrderMessage.class); orderId orderMsg.getOrderId(); // 幂等性保障INSERT IGNORE结合唯一索引 int affected jdbcTemplate.update( INSERT IGNORE INTO seckill_order (order_id, user_id, product_id, status, create_time) VALUES (?, ?, ?, 0, NOW()), orderMsg.getOrderId(), orderMsg.getUserId(), orderMsg.getProductId() ); if (affected 0) { log.warn(订单{}已存在跳过重复处理, orderId); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // 真实库存扣减乐观锁 int stockResult jdbcTemplate.update( UPDATE product_stock SET stock stock - 1, version version 1 WHERE product_id ? AND stock 0 AND version ?, orderMsg.getProductId(), orderMsg.getStockVersion() ); if (stockResult 0) { throw new OptimisticLockException(库存更新失败版本冲突); } channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); log.info(订单{}持久化完成, orderId); } catch (Exception e) { log.error(处理订单{}失败, orderId, e); try { // 拒绝消息并重新入队 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } catch (IOException ex) { log.error(消息拒绝失败, ex); } } } }五、架构演进的思考与总结这次架构改造从单体限流到分布式秒杀的演进核心围绕三个原则展开分层过滤、异步解耦、最终一致。分层过滤确保无效请求在离用户最近的地方被拦截不穿透到底层资源。异步解耦让用户感知的延迟降到最低同时保护核心数据层的稳定性。最终一致则是一道安全网保证在分布式环境下数据不丢不错。上线后的监控数据验证了架构设计的有效性峰值 QPS 从之前的 3000 提升到 18000平均响应时间从 2.3 秒降至 120ms超卖事故从每次活动都有降至零数据库 CPU 使用率峰值从 95% 降至 35%。但也要坦诚地说这套方案仍有改进空间。比如 Redis 预扣库存后如果 MQ 大面积积压用户等待正式订单确认的时间会变长。后续考虑引入库存分段预售 BFF 层预确认机制进一步缩短用户感知的确认延迟。架构演进没有终点每一次活动的复盘都是下一轮优化的起点。作者李然程序员鸭梨Java 架构师专注分布式系统与高并发架构设计。