秒杀系统实战:Spring Boot+Vue实现高并发库存扣减与消息队列削峰 📅 发布时间:2026/9/13 19:25:47 👁 浏览次数: 简介这是一份面向Java毕业设计或期末项目的完整秒杀系统源码包基于Spring Boot与Vue实现前后端分离架构覆盖用户登录、商品列表、秒杀下单、订单管理及高并发处理等核心模块适合想深入掌握高并发场景、数据库优化与项目部署的开发者使用。资源共810个文件压缩包约20MB内含123个Java后端文件、47个Vue前端组件、164个JavaScript脚本以及CSS样式、数据库SQL脚本等完整覆盖从后端接口、前端页面到数据初始化的核心代码同时附有论文文档、答辩PPT和详细使用说明帮助快速理解设计思路与实现细节。开发者可根据说明文档在Windows 10/11环境下运行启动脚本部署项目并对照源码学习秒杀场景中的缓存、数据库事务与并发控制策略。目前已有50人学习下载适合作为毕业设计、课程综合实践或前后端分离项目的参考模板。1. 秒杀系统为什么是 Spring Boot Vue 的试金石秒杀系统表面看只是“限时降价购买”实际是对整个后端并发控制的强制检验。我第一次完整做这个项目时发现问题不在 Spring Boot API 怎么写而在库存判断和扣减的原子性两个请求同时读到库存为 1然后各自执行 update最终卖出两单。后来在毕业设计里把秒杀场景彻底拆了一遍才理解了为什么要用 Redis 预减库存、消息队列削峰以及数据库唯一索引兜底。这套基于 Spring Boot Vue 的前后端分离秒杀系统适合正在做 Java 课设、准备毕业设计答辩、或者想从增删改查往高并发业务靠拢的开发者。项目里除了完整前后端代码还有 SQL 脚本、论文、PPT 和使用说明文档能直接照着部署到本机验证流程比只看零散教程更有价值。2. 秒杀系统的核心链路数据库表设计与库存扣减策略秒杀系统的复杂度通常不是功能多而是并发下数据一致性问题。所以数据库设计一开始就要为高并发留好余地。下面从表结构、库存扣减、消息队列三个层面拆解。2.1 商品、秒杀活动与订单的表结构设计大部分入门项目会把商品库存直接放在商品表里秒杀时更新同一张表。这样的问题在于秒杀只是商品的一种营销状态普通商品和秒杀活动耦合在一起后续想扩展秒杀开始时间、每人限购数量就要不断加字段。我一般会拆成 product、seckill_activity、seckill_order 三张核心表。product 保存商品基本信息seckill_activity 保存活动维度的库存、秒杀价格、开始结束时间seckill_order 记录用户下单结果。CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 商品表; CREATE TABLE seckill_activity ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, seckill_price DECIMAL(10,2) NOT NULL, stock INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0未开始 1进行中 2已结束 ) COMMENT 秒杀活动表; CREATE TABLE seckill_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, activity_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_sn VARCHAR(32) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_activity_user (activity_id, user_id) ) COMMENT 秒杀订单表;这个结构中 activity_id 和 user_id 的联合唯一索引是防重复下单的关键约束。即使上层逻辑出现重复请求数据库也会因为唯一索引报错从而保证同一个用户在同一场活动中只能有一条订单。注意 stock 字段用 int 而非 varchar 或者 decimal因为库存只做整数加减而价格用 decimal(10,2)避免 float 精度问题。项目自带 SQL 脚本的建表逻辑基本一致直接用 MySQL 执行后再根据字段名调整代码即可。2.2 Redis 预减库存用原子 decrement 替代 get-then-set表结构定好后最需要关注的是库存扣减。先查库存再 update 的写法在并发情况下一定会出问题因为两个请求可能同时读到未更新的库存。常见做法就是把库存预存在 Redis 中利用 Redis 单线程执行命令的特点保证 decrement 原子性。下面是后端秒杀接口的第一段逻辑Autowired private StringRedisTemplate redisTemplate; public ResultString executeSeckill(Long activityId, Long userId) { String stockKey seckill:stock: activityId; Long remain redisTemplate.opsForValue().decrement(stockKey, 1); if (remain null || remain 0) { // 已经售罄需要回补被扣掉的库存 redisTemplate.opsForValue().increment(stockKey, 1); return Result.error(已抢光); } // 库存预减成功后续通过MQ异步生成订单 return Result.success(排队中); }这里 decrement 是原子操作不存在“两个线程同时减到同一个值”的问题。当返回值小于 0 时说明库存已经被减超了所以立刻回补 1并提示用户已抢光。需要注意的是这一步只是预减数据库里真实的库存还没有变化因此还必须依赖数据库最终扣减。这段逻辑同时要处理 Redis 不可用的容错严格场景下可以把异常捕获后直接转同步扣减避免因为 Redis 宕机导致整个接口不可用。在方案选型上我见过不少项目直接把 update 语句作为唯一防线也有项目只用 Redis 而完全绕过数据库校验这两种方式在秒杀场景下都有风险。下面这张表对比了三种常见方案的取舍方案能否防止超卖并发请求实现复杂度适用场景同步 update where stock 0能较低低小型后台系统数据库乐观锁 重试能中等中普通抢购Redis 预减 消息队列能高较高秒杀、限量活动对于这个毕业设计项目建议采用第三种方案因为论文里能讲出性能差异答辩时也更容易说清楚“为什么用 Redis”。并且项目说明文档里已经写出了 Redis 环境要求和 Windows 下的启动方式跟着部署即可。2.3 消息队列削峰把高并发写入变成异步队列预减库存之后如果直接在请求线程里去 insert 订单数据库仍然会被瞬时打满。常见做法是把下单请求改成消息队列异步处理。生产者只负责把 activityId、userId 封装成消息发出去消费者在队列消费时再查一次 Redis 缓存并在数据库落单。使用 RabbitMQ 时后端生产者代码可以这样写Autowired private RabbitTemplate rabbitTemplate; public void sendSeckillMessage(Long activityId, Long userId) { MapString, Long message new HashMap(); message.put(activityId, activityId); message.put(userId, userId); rabbitTemplate.convertAndSend(seckill.exchange, seckill.routing.key, message); }消费者监听队列后执行真正的数据库写入RabbitListener(queues seckill.queue) public void handleSeckillMessage(MapString, Long message) { Long activityId message.get(activityId); Long userId message.get(userId); // 更新数据库扣减库存如果影响行数为0则说明已经被抢完 int rows seckillActivityDao.reduceStock(activityId); if (rows 0) { seckillOrderDao.insert(activityId, userId); } }使用消息队列的好处是即使一秒进来一万个请求最终达到数据库的写操作也只有消费者手里的那部分起到削峰填谷的作用。注意消费者中也要做防重校验如果已经存在相同 activityId 和 userId 的订单直接放弃本次消费防止重复下单。项目里如果使用的不是 RabbitMQ也可以替换成 Spring 自带的异步线程池但原理一致不能在高并发请求线程里直接写库。3. 防超卖与防重复提交Spring Boot 与 Vue 的并发边界处理Redis 预减只是把库存压力转移了最终数据一致性仍然要由数据库兜底。这里重点讲乐观锁、接口幂等和前端活动状态控制。3.1 数据库最终防线乐观锁扣减与唯一订单约束在异步下单阶段消费者执行的数据扣减必须带上库存大于 0 的条件不能让库存变成负数。我在工程里一般这样写UPDATE seckill_activity SET stock stock - 1 WHERE id #{activityId} AND stock 0;这条 SQL 是行级锁生效的典型场景多条 update 同时执行时只有一条语句的影响行数是 1其余返回 0。拿到影响行数为 1 的线程才允许插入订单否则直接结束。它不依赖代码里先 select 再 update因此不会出现“检查时库存足够更新时已经卖完”的窗口。这段兜底逻辑是整个秒杀系统最后一道防超卖屏障Redis 即使丢掉了库存数据数据库也不会写错。真正容易被忽略的是订单表唯一约束。很多项目在代码里先 select count 判断用户是否买过再 insert这个操作本身就不是原子性的。两个并发请求同时 select count 都为 0于是产生两个订单。解决办法就是在建表阶段给 activity_id 和 user_id 加联合唯一索引。插入时如果违反约束catch 住 DuplicateKeyException 即可订单表自然保证了“一人只能一单”。配合上面的 reduceStock 更新可以完全消除超卖与重复下单。3.2 接口层幂等利用 Redis SETNX 做用户请求锁即使数据库有唯一索引高并发下重复请求仍然会白白消耗 Redis 连接和 MQ 队列资源。常见的做法是用户维度加锁同一个用户对同一场活动只能有一次有效请求。Spring Boot 里可以直接使用 Redis 的 setIfAbsent 方法String lockKey seckill:user: activityId : userId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (Boolean.FALSE.equals(locked)) { return Result.error(请勿重复提交); } try { // 执行预减库存和发送MQ } finally { redisTemplate.delete(lockKey); }setIfAbsent 对应 Redis 的 SETNX只有第一次调用才会返回 true后续请求会被直接拦截。锁失效时间设置为 10 秒是为了防止线程在发送消息后异常退出导致锁永远不释放。注意如果需要保证“活动期间只能抢一次”不要在 finally 中提前释放锁正确做法是在订单创建成功后再删除锁同时给锁设置一个足够长的过期时间。这里选用 Redis 而不是 JVM 本地锁原因很简单本地锁只对单节点生效项目一旦横向扩容到多台服务器本地锁就形同虚设。3.3 Vue 端活动控制倒计时与按钮防抖前端不需要承担真正的并发控制但必须给用户正确的反馈。Vue 页面里倒计时和按钮状态是最直观的表现。下面是一段简化后的活动状态模板template div classseckill-panel span v-ifcountdown 0剩余 {{ formatTime(countdown) }}/span button :disabled!canKill clickhandleKill {{ canKill ? 立即抢购 : 已结束 }}/button /div /template script export default { data() { return { countdown: 0, status: doing, // 未开始 / 进行中 / 已结束 }; }, computed: { canKill() { return this.status doing this.countdown 0; }, }, methods: { startTimer() { setInterval(() { // countdown 由后端返回的结束时间与本地时间计算得出 this.countdown Math.max(0, Math.floor(this.endTime - Date.now())); }, 1000); }, handleKill() { if (!this.canKill) return; // 这里可以加防抖this.isSubmitting this.$store.dispatch(seckill, this.activityId); }, }, }; /script前端按钮 disabled 只是降低误点击概率不能作为系统的合法判断依据。因为用户可以绕过页面直接调用后端接口所以后端一定还要有 3.2 节那种幂等控制。另一个常见错误是只根据本地时间控制活动状态本地时间可以被用户篡改导致活动未开始就能请求。稳妥的做法是进入页面时先请求后端接口获取服务器时间再计算倒计时。Vue 路由切换时还要记得清理定时器否则组件销毁后 setInterval 仍然在跑会带来性能问题。4. 前后端联调与部署从 Axios 封装到 Nginx 反向代理这一部分关注点在于拿到项目源码后如何快速把前后端跑通。包含依赖配置、环境变量、跨域和 Windows 脚本几个层面。4.1 Spring Boot 后端依赖与 Redis 环境配置使用 Maven 管理依赖时pom.xml 至少要引入 Web、MyBatis、Redis、RabbitMQ 和 MySQL 驱动。许多同学 springboot 版本太高导致启动失败我一般会控制在 Spring Boot 2.7.x 以内因为原生连接池和 Redis 客户端兼容性更稳定。下面是一份常见的 application.yml 关键配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/seckill?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root redis: host: localhost port: 6379 database: 0 rabbitmq: host: localhost port: 5672 username: guest password: guest这里 url 必须带上 serverTimezoneMySQL 8 以上的驱动已经接收不到不带时区的连接。Redis 默认启动后不需要密码如果本机设置了密码要在配置里同步添加 password。RabbitMQ 的 guest 用户只允许 localhost 访问远程联调时记得创建新用户。项目使用说明文档里标注了这些中间件版本严格按文档来能节省很多排查时间。提示如果本地没有安装 RabbitMQ也可以把消费者逻辑替换成 Spring 的 Async演示效果够但论文里要说明为什么选用 MQ。4.2 Vue 环境变量与 Axios 拦截器封装Vue 项目拿到手后第一件事是看根目录有没有 .env.development 和 .env.production。在开发环境里通常这样配置VUE_APP_BASE_APIhttp://localhost:8080/api然后封装 Axios 请求让所有接口自动携带 Token并在 401 时跳回登录页。下面是一段常用的 request.js 写法import axios from axios; import router from /router; const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000, }); service.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); service.interceptors.response.use( (response) response.data, (error) { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } ); export default service;这个拦截器解决了两个问题一是所有请求自动带上登录态不需要每个接口单独写 header二是后端返回 401 时集中处理登录失效避免在每个页面里重复判断。拦截器里 timeout 设置为 10 秒秒杀接口如果使用同步请求建议在部分场景下放宽到 30 秒因为异步下单接口通常先返回“排队中”最终结果由轮询获得。4.3 Spring Boot 跨域配置与 Nginx 反向代理开发环境前后端端口不同浏览器会触发跨域。Spring Boot 中可以写一个统一配置类处理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true); } }不过生产环境更推荐通过 Nginx 做反向代理由同一域名转发请求浏览器就不会产生跨域问题。Nginx 配置中把 /api 前缀转发到 Spring Boot 服务前端静态资源则直接由 Nginx 托管server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location /api { proxy_pass http://127.0.0.1:8080/api; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里把 /api 单独拎出来做代理可以让前端代码里保持 axios baseURL 为 /api部署时不需要重新打包。root 指向的是 Vue 打包后的 dist 目录而 try_files 指令负责 vue-router 的 history 模式刷新重定向否则用户直接访问 /seckill 地址时会得到 404。4.4 Windows 下的一键脚本与启动顺序项目根目录里带了 1-install.bat、2-run.bat、3-build.bat 以及 mvnw.cmd 这类脚本。拿到压缩包后建议按顺序执行先安装依赖再启动后端最后构建前端。1-install.bat 通常会调用 mvn 进行依赖安装并检查 npm 是否存在2-run.bat 启动 Spring Boot3-build.bat 则对应 Vue 项目的 build 流程。需要注意的是执行 build.bat 前必须已经运行过 npm install如果之前装过旧版本依赖建议删除 node_modules 后重新安装避免 vue 打包后布局异常这类问题。真实生产环境不要用 bat 脚本Linux 上应写 shell 脚本并配合 systemd 管理 Java 进程。5. 压测时最容易忽视的四个参数用 JMeter 验证秒杀吞吐量秒杀系统做完后如果不压测很难说出到底能扛多少并发。JMeter 是毕业设计里最常用的压测工具。我在压测时关注四个参数线程数、Ramp-Up 时间、循环次数、超时时间。线程数代表模拟用户数量一般从 100 起步Ramp-Up 时间如果设为 10 秒意味着 100 个请求会在 10 秒内逐步发送而不是同时冲击。真正需要看的数据是聚合报告里的 Throughput 和 Error %。如果错误率超过 5%优先检查是不是 MySQL 连接池满了其次是 Redis 连接配置。常见的压测配置是 300 线程、Ramp-Up 5 秒、循环一次。这时可以观察 Redis 库存 key 的递减速率。我习惯在压测过程中同时执行 redis-cli monitor看实际到达 Redis 的请求是否均匀。如果 monitor 输出有大量超时说明 Redis 连接数不够需要在 application.yml 中调大 lettuce 连接池。另一个很容易忽略的参数是 HTTP 请求中的 Connection: keep-alive。JMeter 默认使用 KeepAlive但真实浏览器在高并发场景下也会复用连接。如果压测时关掉 KeepAlive得到的结果会过于悲观。压测脚本中应该给秒杀接口添加额外部断言比如响应 JSON 里的 code 字段不是 200就认为失败。很多项目只盯着 HTTP 状态码结果发现了 500 响应也没有标记为错误最终报告里错误率被拉低。压测完成后的验证技巧秒杀结束后把 Redis 库存与数据库库存做一次对账。redis-cli --raw get seckill:stock:1 mysql -uroot -p -e select stock from seckill_activity where id1;如果两者不一致说明存在消息队列重复消费或者数据库更新失败的情况优先排查消费者是否捕获了异常。把这条对账操作写成一个脚本每次压测后执行一次比盯着界面看可靠得多。本文还有配套的精品资源点击获取