本文目录导读:

- 目录导读
- 秒杀系统的“魔鬼”细节:为什么普通CRUD撑不住?
- 核心架构蓝图:从前端到数据库的层层“降级”策略
- 关键武器库:Redis预减库存 + MQ异步削峰 + 令牌桶限流
- 超卖与重复下单的“终极审判”:数据库乐观锁与唯一索引
- 压测与调优:从1000 QPS到10万 QPS的进阶之路
- 常见问题快问快答(FAQ):面试官最爱问的7个问题
高并发下的“秒杀”艺术:Java秒杀系统案例设计全解析
目录导读
- 秒杀系统的“魔鬼”细节:为什么普通CRUD撑不住?
- 核心架构蓝图:从前端到数据库的层层“降级”策略
- 关键武器库:Redis预减库存 + MQ异步削峰 + 令牌桶限流
- 超卖与重复下单的“终极审判”:数据库乐观锁与唯一索引
- 压测与调优:从1000 QPS到10万 QPS的进阶之路
- 常见问题快问快答(FAQ):面试官最爱问的7个问题
秒杀系统的“魔鬼”细节:为什么普通CRUD撑不住?
秒杀的本质是“瞬间高并发 + 有限库存 + 强一致性”,如果你用传统的select判断库存后再update,在1000个并发下,数据库的行锁会让响应时间呈指数级上升,甚至直接打死连接池。
在案例设计中,我们首先要明确一个目标:把数据库的写压力与并发流量隔离开,核心思想是“快速失败,异步落库”,用户点击秒杀按钮后,系统必须在毫秒级内返回“成功排队”或“已抢完”,而不是让用户傻傻等待数据库事务提交。
核心架构蓝图:从前端到数据库的层层“降级”策略
一个生产级的Java秒杀系统,通常采用分层“挡箭牌”架构:
- 第一层(客户端/网关):按钮置灰 + 验证码 + 隐藏秒杀地址,Nginx配置
limit_req模块,按IP或用户ID进行QPS限制(比如1秒1次)。 - 第二层(应用层前置):本地内存缓存(Caffeine或Guava)存储“秒杀标志”,如果活动未开启或已结束,直接返回“请求非法”,不穿透到Redis。
- 第三层(Redis层):这是核心,库存预减在Redis中完成,使用Lua脚本保证原子性。
- 第四层(消息队列):获取Redis扣减成功的请求,发送到RocketMQ或RabbitMQ,异步消费生成订单。
- 第五层(数据库层):通过数据库唯一索引(
user_id + product_id)防止重复下单,乐观锁(version或stock > 0条件)兜底。
关键武器库:Redis预减库存 + MQ异步削峰 + 令牌桶限流
1 Redis预减库存(Lua脚本)
不要用get再decr,而是直接用Lua脚本保证原子性:
local stock = redis.call('get', KEYS[1])
if tonumber(stock) <= 0 then return 0 end
redis.call('decr', KEYS[1])
return 1
在Java中通过DefaultRedisScript<Long>封装,注意:预减库存前需要先加载商品数量到Redis(set goods:1 100)。
2 异步削峰
成功扣减Redis库存后,不直接查库写订单,而是把消息({userId, productId, time})发到MQ,消费者以5个线程以上批量处理,每100ms刷一次数据库,或者做批量插入,这样数据库的TPS从1万降到几十。
3 限流
除了网关限流,应用层引入Guava RateLimiter(令牌桶)做接口级限流,比如每秒放行1000个请求,如果是分布式场景,用Redisson的RRateLimiter。
超卖与重复下单的“终极审判”:数据库乐观锁与唯一索引
超卖问题:在数据库更新时执行UPDATE seckill_goods SET stock = stock - 1 WHERE id = #{id} AND stock > 0,返回受影响行数为0则不生成订单。
重复下单:在seckill_order表建立联合唯一索引uk_user_product(user_id, product_id),当MQ消费者插入时,如果主键/唯一索引冲突,捕获DuplicateKeyException并认为是重复订单,直接确认消费。
压测与调优:从1000 QPS到10万 QPS的进阶之路
通过JMeter模拟5000并发,初次测试时,你会发现数据库连接池(HikariCP)最大线程数只有10,导致大量等待,调优步骤:
- 提高DB连接池:最大连接数设置为200-300。
- 开启Redis Pipeline:批量查询减少RTT。
- 优化JVM:堆内存设4GB,用G1垃圾回收器,避免Full GC导致全局暂停。
- 分级降级:如果依赖的依赖(如短信服务)不可用,使用Sentinel熔断降级。
在案例实测中,经过上述优化,单机应用可以稳定支撑每秒1.2万请求(秒杀接口的QPS),而数据库写入仅每秒100笔——这足以应对双11等典型场景。
常见问题快问快答(FAQ):面试官最爱问的7个问题
Q1:如果不依赖Redis,纯数据库能不能做秒杀? 可以,但性能极限在每秒几百笔,且连接池容易被打爆,数据库在事务中会锁行,用户排队时间过长。
Q2:如果Redis中库存减成功了,但是MQ消费失败怎么办?
必须引入可靠消息,建议在发送MQ前,将“待下单”状态写入Redis中的seckill_processing字段,当一个定时任务扫描到“消息超过10秒未被消费”,则重新发送MQ。
Q3:秒杀中如何保证库存不会超卖?
数据库写入时使用stock > 0作为where条件,这是最终的原子性保障。
Q4:为什么需要隐藏秒杀接口地址? 防止恶意脚本绕开前端按钮,需要动态生成随机URL,并存入Redis,一定时间内有效。
Q5:如果用户连续点击两次提交按钮怎么办?
前端按钮置灰 + 后端对user_id在Redis中加一个“已请求”标记,使用setnx命令,存在则拒绝。
Q6:Redis和数据库库存不一致怎么办? 通过MQ生产端和消费端的事务力:在发送MQ前写入“本地消息表”(或者使用RocketMQ事务消息),最终一致即可,实际允许订单量稍多于Redis库存,但数据库层面不会超卖。
Q7:如果秒杀时,Redis服务挂了怎么办? 高可用方案:使用Redis集群(哨兵)模式,至少一主两从,同时开启本地进程级缓存(如商品库存预热到JVM),在Redis不可用时直接返回“活动火爆”。