尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SpringBoot高并发秒杀系统实战:Redis预减库存与消息队列削峰方案
1. 秒杀场景的真实痛点为什么普通商城接口撑不住我第一次接触SpringBoot商品秒杀系统是帮某高校一位学弟完善他的毕业设计。当时他的限时抢购商城还是典型的普通CRUD写法——用户点击抢购后端直接update库存表再insert订单表。功能跑通没问题但用压测工具模拟100个并发用户时库存居然变成了负数超卖60多单。那一刻我意识到秒杀系统的难点从来不在业务本身而在于你要用一套工程手段硬扛住那种瞬间涌入的流量同时保证数据不错、系统不崩。这也是为什么这类毕业设计年年都有人做却年年都有不同深度的做法。题目里基于SpringBoot高并发闪购平台这几个词实际上暗含了三条要求技术栈要主流现代、系统要能处理高并发冲击、整体要有完整闭环。如果你只是把CRUD包装一下答辩时老师一问你并发100时的库存一致性怎么保证很容易答不上来。所以这篇文章我会从实际构建的角度把秒杀链路拆开讲清楚——数据库设计、缓存层、消息队列、限流防刷、压测复盘每一层都给出可以直接落地的做法和参数建议。先说清秒杀系统要治的三大顽疾。第一是超卖也就是库存100件却卖出150件这是并发下典型的读改写竞态第二是请求风暴一个秒杀活动开启的瞬间请求量可以达到平时的数百倍Tomcat线程池、数据库连接池都可能被打爆第三是薅羊毛脚本自动点击、提前下单、一人抢多件这些真实场景中非常常见。SpringBoot解决的是快速搭建服务的效率问题但真正决定系统能不能秒杀的是你在应用层、缓存层和存储层做的防护设计。2. 数据库与缓存层的两段式设计先解决超卖和库存2.1 秒杀商品表和订单表怎么建才不容易脏秒杀系统的表结构不能照搬电商通用表必须围绕库存扣减和订单去重做针对性设计。我见过很多初版设计把秒杀库存放在商品表里订单表只存基本字段结果update商品表时锁冲突激烈数据还是经常错。更稳妥的做法是单独拆一张秒杀商品表把秒杀价、秒杀库存、活动开始时间、活动结束时间独立管理。下面是模拟项目X的核心建表SQL直接在MySQL 8.0中执行即可CREATE TABLE seckill_product ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, product_name VARCHAR(128) NOT NULL COMMENT 商品名称, seckill_price DECIMAL(10,2) NOT NULL COMMENT 秒杀价, original_price DECIMAL(10,2) NOT NULL COMMENT 原价, stock_count INT NOT NULL COMMENT 秒杀总库存, start_time DATETIME NOT NULL COMMENT 活动开始时间, end_time DATETIME NOT NULL COMMENT 活动结束时间, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), KEY idx_seckill_start (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT秒杀商品表;CREATE TABLE seckill_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, seckill_id BIGINT NOT NULL COMMENT 秒杀商品ID, user_id BIGINT NOT NULL COMMENT 用户ID, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态 0待支付 1已支付 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, PRIMARY KEY (id), UNIQUE KEY uk_seckill_user (seckill_id, user_id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT秒杀订单表;表里有两个细节值得说道说道。第一seckill_order表加了UNIQUE KEY uk_seckill_user (seckill_id, user_id)这个联合唯一索引就是防止同一个人在同一场活动中重复下单的最后一层兜底。就算你的Java代码出现了并发BUG数据库也会直接报Duplicate entry错误而不是生成两张订单。第二seckill_product表预留了version字段这是为后面做乐观锁方案留的后路——如果你想展示数据库层如何防止超卖这个字段可以直接支撑你的对比实验。2.2 预减库存让Redis扛住第一波流量冲击秒杀活动的流量特点是非常短促往往开抢后几秒钟就会涌入活动期间80%左右的请求。如果每一笔下单都直接去MySQL里UPDATE stock_count stock_count - 1数据库很快就会被锁和连接池耗尽拖垮。实际项目中更常见的思路是两点配合先用Redis做预减库存把真正的库存扣减从MySQL压力下解放出来Redis扣减成功后才允许用户进入下单流程MySQL再根据订单状态做最终数据一致性处理。这里有一个容易犯的错误把Redis当成最终存储来用只在扣减后简单写一条订单同步不完整数据库里的库存和Redis库存就容易对不上。正确姿势是把Redis看成令牌闸门MySQL看成事实账本。Redis负责在流量峰值把多余的请求挡在外面真正进入下单流程的请求数量被控制在库存量附近后续异步落库时数据库自己并没有扛住百万级请求它只需要处理数千笔真实有效的订单写入压力完全可控。具体到扣减逻辑我推荐用Lua脚本而不是Java代码里先get再set两步操作。原因很简单多线程环境下get和set之间存在时间窗口两次get到同样的值再分别减一Redis库存会越减越少甚至变成负数。Lua脚本在Redis单线程模型中是原子执行的可以一次完成检查并扣减安全可靠。下面的脚本是我在模拟项目中实际使用的核心片段private static final DefaultRedisScriptLong STOCK_SCRIPT new DefaultRedisScript( if redis.call(get, KEYS[1]) false then return -1; end; if tonumber(redis.call(get, KEYS[1])) 0 then return -2; end; return redis.call(decrby, KEYS[1], 1);, Long.class );调用时传入的KEYS[1]格式为seckill:stock:{商品ID}脚本有三种返回值-1代表活动不存在或Redis键未初始化-2代表库存已售罄大于等于0的数字代表扣减后的剩余库存。只有返回0或正数时才继续往下走否则直接返回已抢完。这一小段逻辑是整个秒杀系统的高速入口也是我在多次压测后确认性能最好的方案之一。2.3 为什么还要同步库存到数据库最终一致性不能丢Redis预减库存可以挡住高并发但Redis终归是内存型存储一旦宕机又没有持久化已扣减的库存和实际发放的订单可能全部错乱。所以订单落库时数据库里的库存也必须同步扣减只是扣减时机和方式可以推后、可以批量。生产环境中常用的方式包括MySQL事务里update seckill_product加WHERE stock_count 0条件或者通过消息队列消费订单时在事务内执行库存扣减和订单状态更新。这个同步扣减的过程需要配合状态机来做。我建议给订单增加一个已预减库存但未落地的中间状态比如在Redis扣减成功后立刻生成一条状态为0待支付的订单记录支付成功后状态变成1如果支付超时或者用户取消则把Redis库存回补。这里有个很重要的回补注意点回补库存必须加锁否则多个取消订单同时回来后Redis库存会被错误地加多。我在开发模拟项目X时就遇到过一次A用户和B用户几乎同时取消订单两个回补请求都读到剩余库存为0各加1后变成2系统多卖了一件。如果你想让毕设展示出更成熟的一致性设计可以这样设计流程闭环Redis预减库存 - 创建待支付订单 - 支付宝/微信模拟支付回调 - 更新订单状态 - 异步扣减MySQL库存 - 如果超时未支付则回补Redis。每一步都有明确的可视化结果答辩演示时非常有说服力。3. 核心秒杀链路从限流到异步下单的三层防护3.1 入口防刷与限流别让系统死在无效请求里高并发场景下真正需要保护的不仅是数据库还有应用服务本身。我压测时发现一个很有趣的现象在没有入口限流的情况下500个并发就可以把Tomcat默认线程池打到接近满负荷而其中有大量请求是重复点击、脚本请求和违规提前请求。SpringBoot默认配置中Tomcat工作线程数大约是200等待队列长度100一旦超额新请求会直接被拒绝或者排队到超时。入口层我建议做三件事。第一秒杀地址动态化不要在页面静态资源里写死接口路径而是用户点击抢购按钮后先请求一个获取秒杀地址的接口由后端生成带随机串的路径过期时间设置为5分钟左右。第二加图形验证码或滑块验证码用户从提交抢购到输入验证码之间通常需要1到2秒这个时间差可以把脚本请求自然过滤掉一大部分。第三使用Spring AOP或拦截器做简单的接口限流比如基于Guava RateLimiter控制单个秒杀接口的令牌发放速率也可以按用户维度在Redis里维护一个1秒内的访问次数计数器。我做毕设模拟项目时限流参数一般这样设置秒杀接口整体QPS上限设为300单个用户5秒内只能请求3次秒杀地址获取接口不限流因为它本身很轻量。这个配置配合后端的库存预减压测表现已经可以看出明显改善——之前并发下Tomcat队列堆积上千个请求加了限流后无效请求大量被前置返回错误率显著下降。3.2 核心下单逻辑库存检查、用户去重、订单生成三步分离秒杀核心逻辑如果一股脑写在一个方法里又做库存检查又做订单插入又做支付回调事务范围拉得很大锁竞争会非常明显。我改造A同学的初版代码时最重要的一步就是把下单链路拆成三块前置校验、执行扣减、异步落单。前置校验阶段负责所有不需要访问数据库的检查包括用户是否登录、活动是否在有效期内、用户是否已经下过单查询Redis中的已购标识seckill:user:{userId}:{seckillId}。执行扣减阶段使用上面提到的Redis Lua脚本扣减成功后再通过分布式锁防止同一用户并发抢购。异步落单阶段把订单消息发送到消息队列消费者拿到消息后执行创建订单、扣减MySQL库存、清理用户已购标识等操作。用代码来表示就是这样一个方法骨架public ResultLong handleSeckill(SeckillRequest request) { // 1. 前置检查用户登录态、活动时间、已购标识 if (!checkUserLogin(request.getUserId())) return Result.error(未登录); if (!checkTimeValid(request.getSeckillId())) return Result.error(活动未开始或已结束); if (hasPurchased(request.getUserId(), request.getSeckillId())) return Result.error(每人限购一件); // 2. Redis Lua预减库存 Long stock stockService.deductStock(request.getSeckillId()); if (stock null || stock 0) return Result.error(已抢完); // 3. 发送异步下单消息 seckillProducer.sendOrderMessage(request.getUserId(), request.getSeckillId()); return Result.success(抢购成功请完成支付); }这里有一个很多资料里没写清楚的设计点异步下单消息发出后用户立刻返回抢购成功其实是有点风险的做法因为订单此时还没有真正创建。更稳妥的是服务端立即返回排队中状态前端每1到2秒轮询一次订单状态等状态变成待支付再提示用户付款。这样即使用户量很大也不会因为同步等待订单落库而拖慢主流程。3.3 异步削峰消息队列到底给系统带来了什么消息队列在秒杀链路里扮演的是蓄水池角色。每个秒杀请求都同步访问MySQL即使库存预减已经挡住了大量无效请求剩余的有效请求同时落库依然可能让数据库连接池被打满。通过消息队列把响应用户和落库写订单两个动作拆开用户侧只感知到毫秒级返回数据库侧则在1到2秒内匀速消费消息完成写库流量从尖峰变成了一条平滑的曲线。以RocketMQ为例在SpringBoot项目中核心配置很简单。生产者端在GZIP压缩和批量发送之外还要设置合适的发送超时时间和重试次数消费者端重点设置consumer.setConsumeThreadMin和consumeThreadMax来控制消费并发度一般设置为数据库最大连接数的60%到80%比较合适。我建议消费线程数设置为16到24个之间订单消息消费核心逻辑里再开启本地事务这样既保证性能又保证订单和库存的一致性。很多人会质疑毕业设计有必要引入消息队列这种重量级组件吗我的回答是要看你想拿多少分。如果你的目标是能跑、能演示、能答辩用SpringBoot自带的Async异步方法配合线程池也能实现一定程度的削峰。但如果你的题目里明确写了高并发场景这类关键词那么引入一个具体的消息队列组件并且能解释清楚它如何削峰、如何保证最终一致性这几乎是必答题。RocketMQ在本地Windows和Linux上部署都不复杂占用资源也可控我也试过ActiveMQ和RabbitMQ如果要兼顾文档完善度和中文资料丰富度RocketMQ是更顺手的选项。4. 压测数据与极限瓶颈复盘从压测工具到优化方向4.1 用JMeter搭出逼近真实的并发场景压测是整个秒杀项目里最能把问题暴露出来的环节也是答辩时可以拿出手的硬核数据来源。我常用的压测工具是Apache JMeter因为它的线程组配置直观汇总报告一目了然。要模拟秒杀那种瞬间冲击核心配置是线程数500、Ramp-Up Period设为0秒循环次数1次。Ramp-Up为0意味着500个线程在同一瞬间全部启动这和真实秒杀的点击峰值特性最接近。另外别忘了添加强化配置比如HTTP请求默认值、Cookie管理器模拟登录状态、JSON断言判断返回结果这些细节帮你排除因为压测脚本本身缺陷导致的虚假失败。如果你想跑更高压强的测试还可以用wrk在本机绕过Tomcat并发多端口直压或者用Apache Bench做简单的请求延迟观察。不过毕设演示场景里JMeter的可视化图表往往更直观。我个人习惯是先把测试URL截出来在压测时用单独的监控面板观察CPU、内存、Redis内存变化这样定位瓶颈时会快很多。4.2 实测数据对比从600QPS到3250QPS做了什么我在模拟项目X上基于一个通用配置8核16G虚拟机、MySQL 8.0、Redis 6.0默认JVM参数按三种方案分别压测拿到了下面这组模拟数据。说明一下这只是我本地调试时的相对参考不同机器差异很大但趋势很能说明问题。方案并发线程数平均响应时间错误率是否超卖系统最大QPS方案A同步数据库扣减5004860ms21.5%是超卖23件610方案B数据库乐观锁扣减500352ms5.2%否1100方案CRedis预减 消息队列落库50038ms0.8%否3280看到这个对比方案A基本不用解释同步扣减在高并发下必然超卖平均响应时间高达4.8秒用户体验极差。方案B用乐观锁UPDATE ... WHERE stock_count 0 AND version ?禁止了超卖但因为大量请求在事务提交时冲突失败错误率爬到了5.2%而且频繁的数据库连接占用还是让性能上不去。方案C的优势非常明显用户返回快正确率高性能翻了三倍以上。压测时我还额外把并发抬升到1000方案C的错误率维持在1.5%以内系统CPU的负载稳定在70%左右没有再出现数据库死锁。4.3 压测过程中暴露的三个典型问题第一Redis连接池被耗尽。SpringBoot默认的Lettuce连接池配置非常保守在500并发下很容易出现RedisConnectionFailureException。我后来在配置文件中手动调整了spring.redis.lettuce.pool.max-active50、max-idle20、time-between-eviction-runs30s问题立刻缓解。注意这个值不是越大越好过大的连接池反而会引入多余的线程切换开销让Redis端的QPS不升反降。第二Tomcat线程池排队导致超时请求刷屏。压测时并发一高很多请求在Tomcat队列里等待超过5秒后被前端判定超时前端马上发起重试反而把系统拖得更垮。我通过两个手段解决一是把server.tomcat.threads.max从默认的200适当调大到400同步调大accept-count二是在入口限流中加强了对同一个用户重复请求的熔断重试请求会被直接识别并拒绝。第三MySQL连接耗尽可能出现在任何一环。排查时我看了两个关键指标慢日志中的UPDATE语句是否频繁以及SHOW PROCESSLIST里是否有大量Sleep连接。最终我确认问题主要出在订单表创建时没有使用批量插入每笔消息消费一次就建立一个新的数据库连接。改为连接池复用后这个问题基本消失。压测过程中学会通过top、redis-cli --stat、SHOW ENGINE INNODB STATUS这三个命令组合定位瓶颈是我做这个项目最大的收获之一。5. 毕业设计答辩里容易被追问的四个加分设计5.1 为什么是Redis预减而不是纯数据库锁这是我在交流时几乎必被问到的问题。很多同学能说出Redis快、数据库慢但答不出更深一层。本质原因有两个。第一锁的粒度不同。数据库行锁在秒杀场景下会锁住整个库存行所有下单操作串行化吞吐量天花板很低Redis的单线程模型配合Lua脚本天然串行执行扣减命令单位时间可以处理数万次原子操作而且因为是在内存里运行单次操作耗时只有零点几毫秒。第二流量隔离效果不同。Redis预减库存后大量请求在缓存层就被终止数据库只接收数量可控的有效请求这个流量漏斗的架构思想比单纯调优SQL更先进。答辩时你可以用一句话总结预减库存不是为了把数据算得更准而是为了让数据库不面对它扛不住的流量。5.2 一人一单的限制如何落到存储层而不是软件层秒杀活动最常见的规则是每人限购一件。很多人的实现是Java代码里先查订单表再决定是否插入这个逻辑在并发下必然存在窗口期。正确做法有三层Redis的已购标识先挡一遍消费侧校验再用唯一索引兜底最后利用数据库本身的UNIQUE KEY保证同一用户同一场活动只能有一条订单记录。这其实和前文表的uk_seckill_user对应哪怕Java层漏判了数据库也会拒绝重复插入。能把这个思路讲清楚说明你对并发下的最终一致性机制理解是到位的。5.3 支付超时和订单取消后的数据回补秒杀系统如果只做到下单不做支付超时关闭和库存回补在答辩时会被认为闭环不够完整。常见做法是启动一个定时任务每分钟扫描一次待支付订单如果超过10分钟仍未支付就把订单状态改为已取消并把Redis库存回补、删除用户已购标识。更高效一点的做法是把订单创建时间写入延迟消息队列用延时消息触发关闭不过定时任务方式更简单可控作为毕设演示足够了。我还要提一个细节回补库存时要避免重复回补我的方案是更新订单状态时加一个条件WHERE status 0通过数据库自带的行级锁保证只有第一个更新请求能把订单从待支付改为已取消后续请求因为状态已变更而失败从而保证回补逻辑只执行一次。5.4 加入简单的监控与可视化性价比极高毕设系统如果只有后端接口和前端页面答辩时展示起来会比较干。我建议加一个轻量级的秒杀日志表和监控页面用定时任务把每场活动的请求总数、成功下单数、库存剩余量、接口响应时间统计到一张汇总表里前端用条形图或表格展示出来。这个小功能代码量不高但演示时一目了然也能帮你应对你的系统如何评估效果这类问题。我自己在模拟项目中用ECharts做了三张趋势图分别展示库存变化曲线、请求成功率、接口平均耗时答辩时老师直接对着图问了几个问题比看一堆接口说明更省力。6. 我在整个项目里踩过最深的三个坑第一个坑是Redis键过期时间导致的活动开始瞬间大量请求报错。我最初给秒杀库存键设置了10分钟过期时间结果活动还没结束库存键就过期了前端全部返回已抢完。后来我改成键的过期时间绑定活动结束时间并且派发一个后台任务在活动结束后主动删除键彻底解决了这个问题。经验是要区分缓存设置过期时间和业务数据生命周期两个概念秒杀库存是业务数据不能简单按缓存处理。第二个坑是消息队列消费失败后的无限重试。某个订单消息因为数据库连接异常一直在消费失败RocketMQ默认重试16次后进入死信队列期间用户订单状态一直是排队中体验极差。我改进的方法是消费逻辑增加重试次数上限判断超过5次后主动把消息投递到一个异常订单检查表由定时任务去补偿处理同时前端轮询接口对超过10秒还没出结果的订单直接提示请稍后查询订单列表。这种最坏情况兜底意识其实比正常流程设计更考验人。第三个坑是前端活动开始时间与服务器时间不同步。用户在本地抢到毫秒级提前的请求接口判断活动未开始直接拒绝了。解决方式是前端不依赖本地时间而是活动开始前从后端接口获取服务器时间差用轮询校准后再开启抢购按钮。这个细节花了我大半天才定位到却是一个实战场景中非常经典的问题写进文档里会让你的项目更显真实。如果你正在做类似的毕业设计我建议你把重点放在我上面讲的三个层次第一层数据库防超卖要能实现并讲清原理第二层Redis预减库存要能展示压测数据的提升第三层消息队列和异步落单要能解释削峰的价值。能走完这三层这个题目基本就能撑起一篇扎实的论文和一次高分答辩了。
RELATED

相关推荐

Flutter for OpenHarmony闪屏实现:从启动链路到代码落地

Flutter for OpenHarmony闪屏实现:从启动链路到代码落地

1. 项目背景:为什么一个二手置换App要把闪屏当正经事做先说这个项目本身的来头。我们团队接的是一个校园和社区场景下的二手物品置换App,核心功能包括闲置信息流、商品发布、站内聊天、交易订单这几块,无论是学生毕业季的旧书、电器转手&…

📅 2026/10/16 3:36:35
Code Agent重塑研发协作:工程产品设计三方的实践与踩坑指南

Code Agent重塑研发协作:工程产品设计三方的实践与踩坑指南

最近一年,我几乎所有跟代码相关的工作都在跟Code Agent打交道。作为一个从工程师转技术管理的从业者,我亲眼看着AI写代码从玩具变成了团队里离不开的生产力工具。这篇不是介绍某个工具怎么装,而是想聊聊Code Agent到底改变了什么:…

📅 2026/10/16 3:36:35
多人联网休闲游戏开发实战:状态同步与断线重连实践

多人联网休闲游戏开发实战:状态同步与断线重连实践

最近业余时间做完了一款多人联网休闲游戏的Demo,玩法一句话就能说清:4个玩家进入同一个房间,用各自的小方块在共享地图上抢格子,60秒一局,谁占的面积大谁赢。就这么一个简单的"占格子"玩法,真正连…

📅 2026/10/16 3:36:35
MORE NEWS

更多资讯

📰

Tailwind原子类组合逻辑实战沙盒:67个可运行HTML示例

简介:本资源是Tailwind CSS 6.23版本的完整组件实践包,面向前端开发者、UI工程师及Vue技术栈学习者,旨在解决响应式界面快速搭建、无障碍组件复用与主题定制效率低等实际开发痛点。压缩包含2007个文件,主体为1304个HTML示例页&…

📰

一套播放器组合,让手机、电脑、电视听歌歌库无缝同步

最近好几个朋友都在问我同一个问题,说手机里装了听歌的App,电脑上又装了一两个播放器,电视上还得再装一个,曲库各管各的,会员还不互通,想找一首老歌得来回切换好几个软件。问我说到底有没有“一个软件全搞定…

📰

纯真IP库离线解析指南:数据格式、Python实现与缓存更新

我最早接触纯真IP库,是很早以前在给一个用户量并不大的社区站做访问统计。当时统计脚本里接的是第三方在线IP查询接口,每天配额有限,到了晚上高峰时段经常超限,日志里一票查询失败。后来换成纯真IP库这种离线方案,数据…

📰

DNAstar安装全指南:Windows环境依赖、模块化部署与避坑实战

简介:本资源为DNASTAR Lasergene 7.1.0完整Windows安装包,面向生物信息学研究者、分子生物学实验人员及高校相关专业师生,解决基因组序列分析、引物设计、蛋白结构预测与高通量测序数据组装等核心科研需求。压缩包共2个文件,含35.…

📰

AI辅助逆向工程实战:流程、案例与避坑指南

最近圈子里聊得最多的一个话题,就是“在AI面前万物皆可逆向”。这句话乍听有点像口号,但我这一年多高强度用AI辅助做逆向分析之后,越来越觉得它说的不是玄学,而是实实在在的工作方式变了。以前我们啃一个没符号的二进制&#xff0…

📰

节点有资源却调度失败?Flink TaskManager Pending根因解析

1. 现场还原:节点明明有空余,TaskManager 却一直 Pending1.1 一个典型的“灵异事件”前几天组里的同事火急火燎找我,说 Flink 作业提交到 Kubernetes 集群后,JobManager 倒是起来了,TaskManager 的 Pod 却一直卡在 Pen…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬