news 2026/9/13 19:25:42

秒杀系统实战:Spring Boot+Vue实现高并发库存扣减与消息队列削峰

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
秒杀系统实战:Spring Boot+Vue实现高并发库存扣减与消息队列削峰

简介:这是一份面向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 Result<String> 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) { Map<String, 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(Map<String, 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 class="seckill-panel"> <span v-if="countdown > 0">剩余 {{ formatTime(countdown) }}</span> <button :disabled="!canKill" @click="handleKill" >{{ 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root redis: host: localhost port: 6379 database: 0 rabbitmq: host: localhost port: 5672 username: guest password: guest

这里 url 必须带上 serverTimezone,MySQL 8 以上的驱动已经接收不到不带时区的连接。Redis 默认启动后不需要密码,如果本机设置了密码,要在配置里同步添加 password。RabbitMQ 的 guest 用户只允许 localhost 访问,远程联调时记得创建新用户。项目使用说明文档里标注了这些中间件版本,严格按文档来能节省很多排查时间。

提示:如果本地没有安装 RabbitMQ,也可以把消费者逻辑替换成 Spring 的 @Async,演示效果够,但论文里要说明为什么选用 MQ。

4.2 Vue 环境变量与 Axios 拦截器封装

Vue 项目拿到手后,第一件事是看根目录有没有 .env.development 和 .env.production。在开发环境里通常这样配置:

VUE_APP_BASE_API=http://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 Boot;3-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 id=1;"

如果两者不一致,说明存在消息队列重复消费或者数据库更新失败的情况,优先排查消费者是否捕获了异常。把这条对账操作写成一个脚本,每次压测后执行一次,比盯着界面看可靠得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 19:25:37

MAA 连接正常但无操作时如何通过强制替换 ADB 修复 Minitouch 失效

MAA 连接正常但无操作时如何通过强制替换 ADB 修复 Minitouch 失效 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手&#xff0c;全日常一键长草&#xff01;| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目地址: https://gi…

作者头像 李华
网站建设 2026/9/13 19:22:23

快速上手 RemoveWindowsAI:备份与还原安全网完整指南

快速上手 RemoveWindowsAI&#xff1a;备份与还原安全网完整指南 【免费下载链接】RemoveWindowsAI Force Remove Copilot, Recall and More in Windows 11 项目地址: https://gitcode.com/GitHub_Trending/re/RemoveWindowsAI 删完之后想反悔怎么办&#xff1f;RemoveW…

作者头像 李华
网站建设 2026/9/13 19:22:22

brpc bvar 完全指南:多线程计数器库的原理、使用与监控导出

brpc bvar 完全指南&#xff1a;多线程计数器库的原理、使用与监控导出 【免费下载链接】brpc brpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Rec…

作者头像 李华
网站建设 2026/9/13 19:21:16

完整的方案与示例代码,按照“命令模式 + 执行器 + 可插拔解析器”抽象通信功能,满足高效、可维护、可扩展的需求。设计要点先列出,然后给出必要的代码文件(可直接复制到项目)

完整的方案与示例代码,按照“命令模式 + 执行器 + 可插拔解析器”抽象通信功能,满足高效、可维护、可扩展的需求。设计要点先列出,然后给出必要的代码文件(可直接复制到项目)。 总体方案(简明) • 抽象命令(ICommand):每个与下位机的动作为一个命令对象,包含构建请求…

作者头像 李华