Spring Boot秒杀系统实战:库存扣减、限流与异步下单防超卖
简介这是一套基于SpringBoot的电商秒杀系统完整项目源码面向计算机相关专业的在校学生、教师及企业开发者尤其适合作为毕业设计、课程设计或项目立项演示的参考方案。项目采用MySQL、SpringBoot、Redis与RabbitMQ技术栈重点解决高并发场景下的超卖与重复购买问题通过SQL行锁判断、数据库唯一索引以及消息队列三重手段保障数据一致性是理解秒杀业务与分布式中间件协作的典型实战案例。资源包共147个文件约1.57MB以67个Java源文件为核心业务实现辅以22个HTML页面、12个JavaScript脚本与10个CSS样式文件构成前端交互另含XML配置、SQL脚本及图片资源结构完整便于二次开发。目前已有129人学习下载代码均经过测试运行成功读者可据此掌握秒杀流程设计、缓存与消息队列的落地方式并在此基础上修改扩展其他功能。1. 秒杀系统到底难在哪从“超卖一单”说起电商秒杀系统表面看是“把商品挂上去、到点开抢”实际做起来最扎心的不是功能多而是同一毫秒里几千个请求同时扣同一件库存。我见过一个模拟项目库存 100 件活动结束卖出 137 单财务对账时才发现——这就是典型的超卖。基于 Spring Boot 的秒杀系统核心要解决三件事库存不能超卖、请求不能打垮数据库、下单结果要能追溯。它适合已经会写增删改查、但没处理过高并发场景的后端开发者也适合想拿一个完整项目练手的学生。标题里的“源代码文档说明”意味着这套东西不是纸上谈兵而是能跑起来、能改参数、能压测的工程骨架。接下来我按“先立住理论、再动手复现”的顺序把库存扣减、限流、异步下单、对账补偿这几块拆开讲中间会给出可直接抄的代码和参数也会说清楚哪些地方我翻过车。2. 库存扣减与超卖防护从数据库行锁到 Redis 预扣秒杀系统的第一道命门是库存。很多人第一版会写成“先查库存再判断大于 0再更新”这在单线程下没问题一旦并发上来两个线程同时查到库存 1都判断通过最后卖出 2 件。要解决它得先理解数据库层面的锁行为再决定要不要把扣减挪到 Redis。2.1 数据库悲观锁与乐观锁的选型理由在 MySQL InnoDB 里SELECT ... FOR UPDATE会对行加排他锁直到事务提交才释放。把扣减逻辑放进一个事务先锁行再判断库存就能避免超卖。但秒杀场景下大量请求会在锁上排队数据库连接池很快被占满响应时间飙升。乐观锁用版本号或库存字段做条件更新UPDATE stock SET count count - 1 WHERE id ? AND count 0靠数据库的行锁保证原子性不需要显式加锁吞吐比悲观锁好一些。我一般会先上乐观锁因为实现简单、不依赖额外组件等压测发现数据库 CPU 打满再考虑 Redis。-- 乐观锁扣减一条 SQL 完成判断与扣减 UPDATE seckill_stock SET stock_count stock_count - 1, update_time NOW() WHERE goods_id #{goodsId} AND stock_count 0;这条 SQL 的关键在于stock_count 0放在 WHERE 里数据库执行更新时会加行锁判断和扣减在同一个原子操作内完成。返回值是受影响行数如果为 0说明库存已空或商品不存在直接返回“已售罄”。参数上要注意goods_id必须有唯一索引否则行锁会升级为表锁并发直接崩掉。另外事务隔离级别用默认的 REPEATABLE READ 即可不需要改成 READ COMMITTED因为这里不涉及范围查询。2.2 Redis 预扣库存的 Lua 脚本与回补逻辑当数据库扛不住时常见做法是把库存预热到 Redis用 Lua 脚本保证“判断扣减”的原子性。Lua 脚本在 Redis 里单线程执行天然不会并发穿插。下面这段脚本先检查库存是否大于 0再扣减最后返回剩余值。-- seckill_stock.lua -- KEYS[1]: 库存 key如 seckill:stock:1001 -- ARGV[1]: 本次扣减数量通常为 1 local stock redis.call(GET, KEYS[1]) if not stock then return -1 -- 库存未预热 end if tonumber(stock) tonumber(ARGV[1]) then return 0 -- 库存不足 end return redis.call(DECRBY, KEYS[1], ARGV[1])在 Spring Boot 里用RedisTemplate执行脚本需要把脚本加载成DefaultRedisScript并指定返回类型为Long。参数说明KEYS[1]是商品维度的库存键建议加业务前缀避免冲突ARGV[1]固定为 1如果支持一次买多件再传具体数量。返回值 -1 表示没预热0 表示售罄大于 0 表示扣减成功并返回剩余库存。注意 Redis 预扣成功后数据库扣减可能失败所以必须有一个“回补”机制下单失败时把 Redis 库存加回去或者用消息队列异步补偿。我一般会在业务层记录一条扣减流水定时任务扫描“Redis 已扣、DB 未扣”的记录做回补避免少卖。2.3 库存预热与缓存一致性检查预热通常在活动开始前 5 到 10 分钟执行把数据库库存读出来写入 Redis。这里有个坑预热后如果运营在后台改了库存Redis 和数据库就不一致了。常见做法是后台改库存时直接删 Redis 键下次请求重新预热或者用定时任务每 30 秒同步一次。我一般会加一个校验接口活动开始前手动触发一次“Redis 库存 vs 数据库库存”比对差值超过阈值就告警。参数上预热脚本的批量大小建议 100 到 200 个商品一批避免单次写入过多导致 Redis 阻塞。3. 请求削峰与异步下单把同步链路拆成两段库存扣减解决的是“不超卖”但秒杀瞬间的流量可能是日常的几百倍如果每个请求都同步走完“查商品、扣库存、写订单、发通知”数据库和线程池都会被拖垮。这一章讲怎么把请求先接住、再慢慢处理。3.1 接口限流令牌桶与漏桶在秒杀入口的取舍限流是秒杀系统的第一道闸门。常见算法有令牌桶和漏桶令牌桶允许一定程度的突发流量漏桶则强制匀速。秒杀场景下我一般用令牌桶因为活动开始那一秒的突发是预期内的只要不超过桶容量就行。Spring Boot 里可以用 Guava 的RateLimiter做单机限流分布式环境则用 Redis Lua 实现。下面是一个基于 Redis 的简单令牌桶脚本。-- rate_limiter.lua -- KEYS[1]: 限流 key如 seckill:limit:1001 -- ARGV[1]: 桶容量 -- ARGV[2]: 令牌生成速率个/秒 -- ARGV[3]: 当前时间戳秒 -- ARGV[4]: 本次请求令牌数 local capacity tonumber(ARGV[1]) local rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local requested tonumber(ARGV[4]) local bucket redis.call(HMGET, KEYS[1], tokens, last_time) local tokens tonumber(bucket[1]) or capacity local last_time tonumber(bucket[2]) or now local delta math.max(0, now - last_time) local filled math.min(capacity, tokens delta * rate) if filled requested then return 0 -- 限流 end redis.call(HMSET, KEYS[1], tokens, filled - requested, last_time, now) redis.call(EXPIRE, KEYS[1], 2) return 1参数说明桶容量建议设为“库存数 × 2 到 3 倍”因为不是每个拿到令牌的请求都会最终下单速率按压测得出的数据库承受能力来设比如数据库每秒能处理 500 次扣减速率就设 500。返回 0 表示被限流直接给前端返回“当前人数过多请稍后再试”。注意EXPIRE设 2 秒是为了让冷 key 自动清理避免 Redis 内存被大量限流键占满。3.2 RabbitMQ 异步下单消息体设计与消费幂等限流之后通过的请求不应该同步写订单而是发一条消息到 MQ由消费者慢慢处理。消息体里至少要带用户 ID、商品 ID、活动 ID、请求时间戳、一个全局唯一的请求 ID。请求 ID 用来做幂等防止消息重复消费导致同一用户下两单。下面是一个消息生产者的核心代码。// 发送秒杀消息请求 ID 用于消费端幂等 public void sendSeckillMessage(Long userId, Long goodsId) { String requestId UUID.randomUUID().toString(); SeckillMessage msg new SeckillMessage(); msg.setUserId(userId); msg.setGoodsId(goodsId); msg.setRequestId(requestId); msg.setTimestamp(System.currentTimeMillis()); rabbitTemplate.convertAndSend(seckill.exchange, seckill.order, msg); }消费端收到消息后先拿requestId去 Redis 查是否已处理用SETNX设置一个 5 分钟过期的键设置成功才继续走数据库扣减和订单写入。参数上requestId建议用 UUID不要用“用户 ID商品 ID”因为同一用户可能重复点击。消费端的并发数不要设太大一般 5 到 10 个消费者线程就够设多了反而会把数据库打满。我踩过的坑是消息体用了默认的 Java 序列化结果消费端反序列化失败后来统一改成 JSON 序列化才稳定。3.3 订单落库与状态机从“待支付”到“已取消”异步下单后订单状态不能只有“已创建”。一个可靠的秒杀订单至少有这几个状态待支付、已支付、已取消、已超时。用户下单后进入待支付超过 15 分钟未支付就自动取消并回补库存。状态流转要用条件更新比如UPDATE orders SET status CANCELLED WHERE order_no ? AND status PENDING避免并发操作把已支付的订单改成取消。参数上超时时间建议 10 到 15 分钟太短用户来不及支付太长库存被占用。回补库存时要注意先改订单状态成功后再回补 Redis 和数据库库存顺序反了会导致库存多出来。4. 避坑与排查那些压测时才暴露的问题秒杀系统的坑大多不在功能而在边界和并发。下面几条是我和同行交流时反复听到的翻车记录每条按“现象→原因→解决”写方便对照排查。4.1 库存扣成负数Redis 与数据库双写不一致现象压测时 Redis 库存显示还有 10数据库库存已经是 -3。原因Redis 预扣成功后数据库扣减失败比如连接超时但没有回补 Redis导致 Redis 库存虚高后续请求继续扣。解决在业务层记录每次 Redis 扣减的流水数据库扣减失败时立即回补 Redis并发送告警同时加一个定时对账任务每 5 分钟比对一次 Redis 和数据库库存差值超过 0 就触发人工检查。4.2 消息重复消费同一用户下了两单现象用户反馈自己只点了一次却生成了两个订单。原因RabbitMQ 在网络抖动时会重发消息消费端没有做幂等。解决消费端用requestId做SETNX设置成功才处理处理完再删除或等过期数据库订单表对user_id goods_id 活动 ID加唯一索引作为最后一道防线。4.3 限流误伤正常用户被挡在门外现象活动开始后很多用户看到“人数过多”但库存还有剩余。原因限流速率设得太低或者令牌桶的 key 粒度太粗把不同商品的请求算到一起。解决限流 key 按商品维度隔离速率根据压测结果动态调整同时加一个白名单机制对已登录且信用良好的用户放宽限制。4.4 数据库连接池耗尽请求全部超时现象压测到 2000 并发时接口响应时间从 50ms 涨到 5s最后全部超时。原因同步链路里每个请求都占一个数据库连接连接池最大连接数只有 20。解决把下单链路改成异步数据库操作只在消费端执行连接池参数按“消费者线程数 × 2”设置比如 10 个消费者就设 20 到 30同时给数据库扣减加超时时间避免慢查询拖死连接。4.5 缓存击穿热点商品 key 过期瞬间打垮数据库现象活动开始前 1 秒大量请求同时查同一个商品Redis 里 key 刚好过期全部打到数据库。解决热点商品的缓存不设过期时间或者用逻辑过期加互斥锁预热时把商品信息、库存、活动状态一起写入避免请求时再查库。5. 压测验证与对账补偿怎么确认系统真的扛住了写完代码只是开始秒杀系统必须压测。我一般用 JMeter 或 wrk 模拟 1000 到 5000 并发观察三个指标接口响应时间、数据库 QPS、Redis 命中率。压测前先把库存预热好压测后检查“Redis 剩余库存 已生成订单数”是否等于初始库存如果不等说明有漏扣或多扣。对账补偿是最后一道保险每天凌晨跑一次全量对账把 Redis 流水、数据库订单、库存变更记录三方比对差异记录写入异常表人工介入。参数上压测并发从 500 开始每轮增加 500直到响应时间超过 1 秒或错误率超过 1%那个点就是当前架构的容量上限。我自己的习惯是每次改完库存扣减逻辑先跑 100 并发验证功能再跑 1000 并发看瓶颈最后跑一次全链路对账。这套流程帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

无人机视角多类别目标检测数据集:从标注格式到训练落地的完整链路

无人机视角多类别目标检测数据集:从标注格式到训练落地的完整链路

简介:这份无人机视角多类别目标检测数据集面向从事航拍视觉算法、无人机自动巡检与智慧城市研究的开发者,提供可直接用于YOLO系列模型训练与验证的标注样本。数据按训练720张、验证192张、测试103张划分,覆盖Bridge、Airplane、Bicycle、Boat…

2026/10/9 16:22:32 阅读更多 →
Oracle补丁包应用实战:从命名解析到验证回滚

Oracle补丁包应用实战:从命名解析到验证回滚

简介:面向 Oracle Database 11.2.0.3 的补丁集更新包,编号 20760997,集成 2015 年 7 月关键补丁更新(CPU),适用于 Linux x86-64 平台。补丁集更新涵盖安全修复、稳定性改进与性能优化,可降低已知…

2026/10/9 16:22:32 阅读更多 →
LSTM+RBF-BP混合模型火灾预测实战:时序信号处理与训练避坑指南

LSTM+RBF-BP混合模型火灾预测实战:时序信号处理与训练避坑指南

简介:PDF文档《基于LSTM和RBF-BP深度学习模型的火灾预测方法》收录于《齐鲁工业大学学报》2020年第3期,面向火灾预测、深度学习与多源信息融合方向的研究者及数据分析学习者。方法针对火灾信号时变非线性、单特征预测误报漏报率高等问题,利用…

2026/10/9 16:21:32 阅读更多 →

最新新闻

聚宽SQL数据同步:基于MySQL中转库的增量管道实践

聚宽SQL数据同步:基于MySQL中转库的增量管道实践

简介:面向量化交易与金融数据分析人员,这份压缩包围绕聚宽平台SQL接口的数据传递与中转操作,整理了一套可直接参考的轻量资料,用于解决从聚宽取数到中转数据库落地的需求。包内共3个文件,包含Python脚本、XML配置及rzr…

2026/10/9 17:30:43 阅读更多 →
OCP 19c 083备考:多租户、RMAN与Data Pump实战要点解析

OCP 19c 083备考:多租户、RMAN与Data Pump实战要点解析

简介:面向Oracle OCP 19c-083考试的复习资料,围绕数据库日志恢复、警报监控、网格基础设施升级、PGA内存管理等核心考点展开。内容基于考试真题整理,对Flashback日志空间回收条件、服务器生成警报的手动清除与历史清理、升级网格基础架构的注…

2026/10/9 17:30:43 阅读更多 →
GPT-4.1 Nano 核心能力与实战效果全景展示:把 Codex auth.json 改到 TaoToken

GPT-4.1 Nano 核心能力与实战效果全景展示:把 Codex auth.json 改到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 17:30:41 阅读更多 →
微电网二次控制中的延迟问题:诊断、稳定性影响与工程补偿策略

微电网二次控制中的延迟问题:诊断、稳定性影响与工程补偿策略

微电网的二次控制,说白了就是给整个系统“扶正纠偏”的环节。一次下垂控制把功率分好了,但也留下了频率偏差和电压偏差;二次控制就是那个负责把偏差抹掉的“执行者”。可问题来了,这台执行者一旦患上了“拖延症”——从采样、通信…

2026/10/9 17:30:41 阅读更多 →
ExtJS组件化框架实战:从创建组件到布局通信与封装复用

ExtJS组件化框架实战:从创建组件到布局通信与封装复用

ExtJS 很早就被贴上了"重、老、贵"的标签,很多人一听到它就直接劝退。但如果你真的花时间用过它的组件体系,会有一个完全不同的感受:这根本不是一袋子散装控件,而是一套纪律严明的组件化框架。刚开始被那一堆类名和配置…

2026/10/9 17:30:39 阅读更多 →
数据库物理模型设计实战:字段类型、索引策略与分区方案落地指南

数据库物理模型设计实战:字段类型、索引策略与分区方案落地指南

简介:这份资源聚焦数据库物理模型设计,面向数据库设计人员、后端开发与数据建模学习者,帮助读者理解如何将逻辑模型落地到实际存储系统,兼顾性能优化、存储效率与数据管理。内容以四种核心设计模式为线索,重点讲解主扩…

2026/10/9 17:29:37 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →