💥 前言:数据库不是许愿池
很多新手做秒杀,逻辑是这样的:
- 用户点击 -> 2. 查询数据库库存 -> 3. 如果库存 > 0 -> 4. 减库存,生成订单。
在并发只有 10 的时候,没问题。
但在并发 10 万的时候,MySQL 会瞬间死锁、CPU 飙升 100%,整个系统宕机。
要实现“亿级并发”(虽然我们只有一台破电脑,但架构要向那个方向设计),必须遵循一个原则:把数据库当大爷供着,尽量别烦它。
🏗️ 一、 架构设计:漏斗模型
秒杀的本质是**“限流”和“异步”**。
流量漏斗图 (Mermaid):
🚀 二、 第一道防线:Redis + Lua 实现原子扣减
Redis 是单线程的,速度极快。但如果我们分两步操作(先 Get 库存,再 Decr 扣减),在高并发下依然会有线程安全问题(超卖)。
这时候,Lua 脚本登场了。它能保证多条 Redis 命令像一条命令一样原子执行。
1. 编写 Lua 脚本 (seckill.lua)
放在src/main/resources下:
-- 参数 KEYS[1]: 商品库存 Key-- 参数 ARGV[1]: 购买数量 (通常是 1)localstock=tonumber(redis.call('get',KEYS[1]))-- 如果库存不存在或小于购买数量,返回 -1if(stock==nil)or(stock<tonumber(ARGV[1]))thenreturn-1end-- 扣减库存redis.call('decrby',KEYS[1],tonumber(ARGV[1]))return1-- 成功2. Spring Boot 调用 Lua
@AutowiredprivateStringRedisTemplateredisTemplate;publicbooleanseckill(StringuserId,StringgoodsId){// 1. 预加载 Lua 脚本DefaultRedisScript<Long>redisScript=newDefaultRedisScript<>();redisScript.setScriptSource(newResourceScriptSource(newClassPathResource("seckill.lua")));redisScript.setResultType(Long.class);// 2. 执行脚本 (原子操作)Longresult=redisTemplate.execute(redisScript,Collections.singletonList("seckill:stock:"+goodsId),"1");// 3. 判断结果if(result!=null&&result==1){// 抢单成功!但这只是在 Redis 里成功了returntrue;}returnfalse;}这一步,我们挡住了 99% 的流量。只有库存数量的人能通过。
🌊 三、 第二道防线:RabbitMQ 削峰填谷
即使通过了 Redis,如果瞬间有 1 万个“成功”请求同时打向 MySQL 去创建订单,数据库一样会挂。
我们需要一个蓄水池—— RabbitMQ。
1. 生产者:发送消息 (Controller 层)
if(redisService.seckill(userId,goodsId)){// Redis 扣减成功后,不直接操作数据库,而是发个消息SeckillMessagemessage=newSeckillMessage(userId,goodsId);rabbitTemplate.convertAndSend("seckill_exchange","seckill_route",message);returnResult.ok("排队中...");// 立即返回给前端,不让用户等}else{returnResult.error("秒杀结束");}2. 消费者:慢慢处理 (Service 层)
消费者可以根据数据库的能力,设置Qos(预取数量),例如每次只取 50 条处理,慢慢写入 MySQL。
@RabbitListener(queues="seckill_queue")publicvoidreceive(SeckillMessagemessage){// 1. 再次判断数据库库存 (防止 Redis 和 DB 数据不一致)Goodsgoods=goodsMapper.getGoods(message.getGoodsId());if(goods.getStock()<=0){return;}// 2. 创建订单 & 扣减真实库存// 这一步必须加事务orderService.createOrder(message.getUserId(),message.getGoodsId());}🎨 四、 前端 Vue3 的配合:把压力留在浏览器
后端再强,也怕前端疯狂F5刷新。前端需要做两件事:
- 按钮置灰:点击“立即秒杀”后,按钮立刻 Disable,防止用户连点。
- 答题/验证码:在点击前强制用户进行图形验证或算术题,拉长用户请求的时间差。
- 轮询结果:因为后端返回的是“排队中”,前端需要每隔 2 秒轮询一次接口,查询订单是否创建成功。
// Vue3 伪代码consthandleSeckill=async()=>{btnDisabled.value=true;// 1. 禁用按钮constres=awaitapi.startSeckill(goodsId);if(res.code===200){// 2. 进入轮询状态startPollingResult();}else{message.error("抢光了!");btnDisabled.value=false;}}🛡️ 五、 总结与进阶
通过这套架构,我们实现了:
- Redis Lua:原子性扣减,防止超卖,抗住高并发读。
- RabbitMQ:异步下单,削峰填谷,保护脆弱的 MySQL。
- Vue3:前端限流,优化用户体验。
如果真的有“亿级”并发,还需要做什么?
- 服务降级 (Sentinel):流量实在太大,直接熔断非核心业务。
- 分库分表:订单表数据量太大,需要 Sharding。
- CDN 静态资源缓存:把 CSS/JS/图片全部推到边缘节点。
Next Step:
别光看,去你的 IDE 里建两个 Spring Boot 项目(一个发消息,一个收消息),装个 Docker 版的 Redis 和 RabbitMQ,亲自跑通这个流程。面试时,这绝对是你的杀手锏!