
简介基于SpringBoot的电商基础秒杀项目是一份面向毕业设计、课程设计、工程实训及学科竞赛的完整工程源码包适合有一定Java Web基础、希望快速搭建秒杀业务场景的学习者。压缩包共2000个文件涵盖Java后端源码40个java、前端页面与样式1240个js、357个css、148个html、项目配置与文档34个txt、34个json、27个xml、113个md以及少量go、py、sh等辅助脚本总计约53.91MB目录结构清晰便于按模块查阅。资源经严格测试运行功能正常答辩评审平均分96分可直接复现项目效果也可作为设计报告或扩展开发的参考蓝本。已有43人学习下载适合用于项目起步、课程作业或竞赛备赛下载后建议先查看说明文件如有使用问题可联系作者获得解答。1. 电商秒杀项目的真实价值不是“会写接口”而是“扛得住并发”把 SpringBoot 电商秒杀项目当成普通 CRUD 来做答辩和压测基本都会翻车。秒杀这个场景下的所有技术选型都冲着同一个问题去几千人同时抢一个 SKU怎么保证不超卖还能让数据库不被打挂。这个标题里的项目表面上是一套基于 SpringBoot 的商城代码加上一个秒杀模块本质上是高并发、缓存、消息队列、幂等控制和事务一致性的组合练习题。适合做毕设、课设、实训、大作业或者竞赛的人把它当骨架再按自己的业务改成能上台演示的完整系统。下面这些内容就是你从拿到这套代码到能压测、能演示、能回答追问的完整路径。2. 秒杀系统的技术拆解为什么默认选 SpringBoot Redis RabbitMQ2.1 秒杀链路的四个阶段抢购、预扣、异步下单、超时释放秒杀和普通下单最大的区别在时间窗活动开始的前几秒所有请求集中打到一个 SKU 上。你不可能在那一刻让每个请求都去写数据库这是最直接的结论。常见的从业方案是把秒杀流程拆成四个阶段每个阶段只解决一个问题。第一阶段是资格校验包括活动时间判断、用户是否已下单、商品是否上下架。第二阶段是库存预扣用 Redis 对库存做原子扣减保证不超卖。第三阶段是异步下单把“抢购成功”这件事丢进消息队列让后台消费者慢慢地创建订单、扣减数据库库存。第四阶段是超时释放用户抢到后不在限定时间内支付就把预扣的库存还回去。这个链路把“高峰期的并发写”变成了“高峰期的并发读加原子扣减”真正落库的动作被削峰到了低谷期慢慢做。你在答辩或者写文档时能讲清楚这一层项目就立住了一半。GitHub 上这类电商项目很多但大多数只做到 CRUD秒杀接口还是同步扣库存问题恰恰出在这里。2.2 数据库扣减为什么扛不住行锁与热点行的代价很多人会把秒杀的难点理解成“库存不够卖”其实真正的问题是“热点的库存那行数据被并发写”。MySQL 的 InnoDB 引擎对同一行的 UPDATE 是行锁串行执行的不管你的连接池开多大几千个请求同时执行UPDATE ... SET stock stock - 1时它们都在等同一把行锁。表现是什么数据库连接被占满等待锁的请求堆积在连接池里连接池超时接着抛出DataAccessException最后用户看到的是一片“系统繁忙”。此时数据库 CPU 可能并不高问题不在计算量而是锁等待。更麻烦的是不带条件的扣减还会把库存扣成负数这是后一章要展开的超卖问题这里先记住结论数据库行锁的串行化天然不适合承载秒杀瞬间的热点写。那乐观锁呢用version字段做 CAS冲突后重试问题是秒杀场景的重试率极高第一次抢购冲突率可能超过 90%重试又放大了数据库压力。悲观锁SELECT ... FOR UPDATE更不可取锁的持有时间等于整个事务时间平均响应时间会非常难看。数据库扣减不是不能用而是应该放在异步消费端做兜底而不是放在用户请求链路里做拦截。2.3 Redis 预扣库存与 RabbitMQ 异步下单的分工Redis 的DECR命令在单线程事件循环里执行同一个 key 的操作天然串行不存在两个线程同时读到旧值的情况。预扣库存时先DECR返回值小于 0 就说明库存耗尽这个判断是原子的。配合内存存储的特性一个热点 key 能承受的 QPS 远超数据库一行记录这就是秒杀接口能撑住并发的关键。RabbitMQ 在这里的作用不是“快”而是削峰填谷。用户请求把“创建订单”的消息丢进队列后立刻返回“排队中”后台消费者按自己的速率处理消息。这样数据库的写入流量是匀速的不再是瞬间尖峰。SpringBoot 项目通过spring-boot-starter-amqp就能把连接工厂和RabbitTemplate装配好你只需要声明队列、写消费者方法。这套组合还有一层好处是故障隔离。Redis 挂了用户最多抢不到不会把数据库打挂RabbitMQ 挂了后台攒着消息不会丢恢复后继续消费。数据库在最底层接收的永远是可控流量这个分层思想比任何单个组件都重要。2.4 四种落地方案对比选型不是越复杂越好如果只做课堂作业直接更新库存就够了如果做竞赛或答辩项目需要向评委解释清楚“为什么用 Redis RabbitMQ 而不是直接在数据库里扣”。下面这张表是我平时给项目选型时用的对比口径方案并发表现超卖风险实现成本建议场景直接 UPDATE 扣库存行锁排队几百 QPS 就顶满不带条件会超卖低后台管理系统乐观锁 重试冲突多重试消耗 DB 连接低低库存充足的低并发场景Redis 分布式锁 同步下单锁等待时间长接口容易超时低中小规模促销Redis 预扣 MQ 异步下单接口能到几千 QPS低DB 层还兜底中高秒杀、抢购类活动秒杀项目选第四种不只是因为它扛得住更因为它每一层都能单独验证。Redis 负责原子扣减MQ 负责异步削峰数据库负责最终一致。SpringBoot 在这里的价值不是性能而是生态——spring-boot-starter-data-redis和spring-boot-starter-amqp这套自动装配机制让 Redis 客户端和 MQ 客户端开箱即用你不用去读一堆原生客户端的配置文档这在毕设和实训的时间约束下是非常重要的。3. 跑通秒杀最小闭环建表、预扣库存、异步下单三步走3.1 先建两张核心表秒杀商品表与订单表秒杀项目不需要一上来就建完整的电商库商品表、用户表、订单表、秒杀活动表太多反而干扰演示。做毕设或实训时我习惯先把最小闭环跑通再往上面加购物车、地址、优惠券这些模块。最核心的两张表如下。-- 秒杀商品表一份数据既给页面展示也当作库存的数据库兜底 CREATE TABLE seckill_goods ( id BIGINT NOT NULL AUTO_INCREMENT, spu_id BIGINT NOT NULL COMMENT 对应商品表中的商品ID, goods_name VARCHAR(128) NOT NULL COMMENT 商品名称, seckill_price DECIMAL(10,2) NOT NULL COMMENT 秒杀价, seckill_stock INT NOT NULL DEFAULT 0 COMMENT 秒杀库存数据库兜底, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, start_time DATETIME NOT NULL COMMENT 秒杀开始时间, end_time DATETIME NOT NULL COMMENT 秒杀结束时间, PRIMARY KEY (id), KEY idx_spu_id (spu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT秒杀商品表;库存字段要叫seckill_stock业务含义是“这个活动准备了 100 件”而不是“商品总库存”。活动结束后数据库库存可以清零但商品的真实库存不能被动这里分开设计是为了避免活动影响普通售卖。version字段先留着后面异步下单时做乐观锁用。-- 秒杀订单表唯一索引是用来防重复下单的最后防线 CREATE TABLE seckill_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT 业务订单号非自增, user_id BIGINT NOT NULL, seckill_goods_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id,seckill_goods_id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT秒杀订单表;订单号不用自增 ID 的原因有两个。一是后续做分库分表、对接支付回调时需要全局唯一的业务单号自增 ID 没法承担这个职责二是前端展示订单号一般有业务规则比如日期加流水UUID 或雪花算法生成即可。uk_user_goods唯一索引是防重复下单的数据库层兜底后面幂等失效时它还能救你一命。3.2 秒杀接口Redis 原子预扣加防重复下单建完表之后核心的秒杀业务逻辑就来了。接口要做到三件事判断活动时间、防止同一用户重复抢、用 Redis 原子扣减库存。下面是 Service 层核心方法Controller 层就是一层薄薄的参数校验。public SeckillResult doSeckill(Long userId, Long seckillGoodsId) { // 1. 活动时间校验 SeckillGoods goods seckillGoodsMapper.selectById(seckillGoodsId); if (goods null || !isBetween(goods.getStartTime(), goods.getEndTime())) { return SeckillResult.fail(不在秒杀时间段内); } // 2. 用 setIfAbsent 做用户维度的幂等锁并发点击只放行第一次 String onceKey seckill:once: userId : seckillGoodsId; Boolean first stringRedisTemplate.opsForValue() .setIfAbsent(onceKey, 1, Duration.ofMinutes(5)); if (!Boolean.TRUE.equals(first)) { return SeckillResult.fail(你已经参与过该商品的秒杀); } // 3. Redis 原子预扣库存decr 的返回值就是剩余库存 String stockKey seckill:stock: seckillGoodsId; Long remain stringRedisTemplate.opsForValue().decrement(stockKey); if (remain null || remain 0) { // 扣成负数说明库存没了把幂等标记和库存都回滚 stringRedisTemplate.delete(onceKey); stringRedisTemplate.opsForValue().increment(stockKey); return SeckillResult.fail(手慢了秒杀已结束); } // 4. 异步通知下单预扣库存与订单落库解耦 MqMessage msg new MqMessage(userId, seckillGoodsId, orderNoGenerator()); rabbitTemplate.convertAndSend(exchange.seckill, seckill.create, msg); return SeckillResult.success(抢购成功请尽快支付); }这段代码有三个参数值得细看。onceKey的 TTL 设成 5 分钟覆盖了“抢到但不支付”的等待期用户取消订单后需要手动删除这个 key 才能再次抢购decrement返回剩余库存判断remain 0而不是remain 0是因为高并发下可能同时把库存扣到 -1、-2回滚时用increment把负值拉回 0不会出现回滚到正数的情况消息里的orderNo提前生成消费者直接用避免在消费端生成导致重复。还有一个容易翻车的细节。setIfAbsent的返回值要判断Boolean.TRUE.equals(first)不要直接写if (first)。某些 Redis 客户端在特定序列化配置下返回的 Boolean 存在拆箱问题自动拆箱时如果为 null 会直接抛 NPE这属于那种排查很久最后发现是玄学的坑。3.3 RabbitMQ 消费者数据库真正扣减与订单落库消息进入队列后消费者要做两件事用“库存大于 0”的条件把数据库库存真正减掉然后插入订单记录。这里不能只依赖 Redis 预扣的结果因为 Redis 和数据库是两个存储任何一方的失败都会导致不一致。Component RabbitListener(queues q.seckill.create) public class SeckillOrderConsumer { Transactional public void onMessage(MqMessage msg) { // 1. 数据库层强制扣减带 stock 0 条件防止并发把库存扣成负数 int updated seckillGoodsMapper.deductStock(msg.getSeckillGoodsId()); if (updated 0) { // 数据库没库存了把 Redis 库存修正为 0避免两边不一致 stringRedisTemplate.opsForValue().set( seckill:stock: msg.getSeckillGoodsId(), 0); return; } // 2. 插入订单唯一索引防重复 seckillOrderMapper.insert(buildOrder(msg)); } }对应的 Mapper SQL 是Update(UPDATE seckill_goods SET seckill_stock seckill_stock - 1, version version 1 WHERE id #{seckillGoodsId} AND seckill_stock 0) int deductStock(Param(seckillGoodsId) Long seckillGoodsId);这里解释了为什么要保留version字段stock 0条件防超卖version字段在订单更新等非秒杀场景中防止并发冲突两者职责不同。消费端标了Transactional一旦插入订单失败整个事务回滚库存扣减也回滚消息会重新进入队列这带来了重复消费的可能后面专门讲怎么处理。这个阶段你可以在日志里加一条打印记录“收到消息 - 库存扣减成功 - 订单创建成功”三个节点。演示时打开 RabbitMQ 管理后台能看到消息积压数量从几千降到几十再降到零这个画面本身就是答辩现场最好的“并发削峰”佐证。3.4 超时未支付自动释放库存定时任务与延迟消息用户抢到订单后不一定支付活动规则一般是“5 分钟未支付自动取消”。释放库存的做法有两种。Scheduled(fixedDelay 30_000) public void releaseExpiredOrders() { // PageHelper.startPage(1, 500)分批扫避免一次性加载几万订单 ListSeckillOrder timeoutOrders seckillOrderMapper.listTimeoutOrders(5); for (SeckillOrder order : timeoutOrders) { // 只有待支付状态能取消防止已支付订单被重复取消 int rows seckillOrderMapper.cancelIfPending(order.getId()); if (rows 1) { stringRedisTemplate.opsForValue() .increment(seckill:stock: order.getSeckillGoodsId()); stringRedisTemplate.delete( seckill:once: order.getUserId() : order.getSeckillGoodsId()); } } }fixedDelay 30_000的意思是任务执行完后再等 30 秒执行下一次和fixedRate的固定频率不同后者在任务执行时间超过间隔时会产生重叠执行定时任务里尽量避免。cancelIfPending的 SQL 要把状态条件写进去UPDATE seckill_order SET status 2 WHERE id ? AND status 0返回行数为 1 才说明取消成功防止已支付订单被误取消。扫表方案在数据量小的时候足够如果秒杀场次多、订单量大更合适的做法是用 RabbitMQ 延迟队列把“订单创建”和“延迟 5 分钟检查”绑定在一起到期自动投递一个释放消息。毕设和实训阶段定时任务够用答辩时提一句“生产环境会换成延迟消息”反而能体现你对方案边界的认识。4. 压力测试与参数整定让秒杀接口在千级并发下不翻车4.1 三个必调配置数据库连接池、Tomcat 线程池、Redis 超时代码写完了能不能扛住并发关键在 SpringBoot 配置。很多人新建项目后保持默认配置直接压测结果连接池被打满误以为代码有问题。实际影响秒杀表现的就三个配置段。spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 redis: timeout: 1000ms lettuce: pool: max-active: 32 max-idle: 8 rabbitmq: listener: simple: prefetch: 50 server: tomcat: threads: max: 200 min-spare: 20 accept-count: 100maximum-pool-size设 20 就够了不要给到 100。秒杀接口同步链路只访问 Redis数据库写入被 MQ 削峰到了消费者线程连接池不需要很大的数字。连接数过多反而增加上下文切换和数据库锁竞争这是新手最容易踩的过度配置。connection-timeout: 3000保证数据库连接异常时快速失败而不是让用户请求无限等待。server.tomcat.threads.max: 200控制的是 HTTP 请求线程。每个请求在 Redis 上的操作是零点几毫秒200 个线程保守估计能撑几千 QPS比默认值 200 其实不高关键是不要压测时一上来就开 1000 线程线程池会直接把请求拒掉。accept-count: 100表示线程耗尽后还有 100 个请求在队列里排队宁可排队也不要立刻返回 500。RabbitMQ 消费者的prefetch: 50表示每个消费者一次最多取 50 条消息。值设太大会造成消息在消费者本地堆积消费端重启时一批消息全部重回队列产生大量重复消费设太小则消费吞吐上不去。50 是个比较稳的起步值后续按订单表写入耗时调整。4.2 用 JMeter 或 ab 做压测看哪些指标怎么判断是不是有效压测工具选择上JMeter 适合做复杂的梯度压测ab适合快速验证。不管用哪个核心是能不能压出“真实用户并发”的效果而不是一梭子打到接口上。先看快速验证的命令# JMeter 命令行执行测试计划jmx 文件由 GUI 配置生成 jmeter -n -t seckill.jmx -l result.jtl -e -o ./report # 或者用 ab 快速压一把5000 个请求200 并发 ab -n 5000 -c 200 \ -H token: demo-token \ http://localhost:8080/api/seckill?seckillGoodsId1userId10086-n 5000是总请求数-c 200是并发数。压测结束后看三个指标Requests per second是吞吐量Failed requests是失败数Time per request的平均值要结合 95% 分位看P95 超过 2 秒说明链路有瓶颈。JMeter 聚合报告里的95th pct才是用户真实体验的参考平均值很容易被少数慢请求拉平。压测时一定要分梯度加压50 并发跑 2 分钟100 并发跑 2 分钟500 并发再跑 2 分钟。观察吞吐量是线性增长、放缓还是直接跌穿。如果从 100 到 500 并发吞吐量反而下降大概率是连接池或线程池配置问题不是代码问题。压测的同时在数据库执行SHOW PROCESSLIST如果看到大量的Sleep连接说明数据库并没有被真实打到流量都被 Redis 和 MQ 扛住了这正是这套架构设计要达到的效果。如果数据库连接数接近max_connections并且出现大量Lock wait timeout说明消费者处理速度跟不上去调prefetch和消费者线程数而不是加数据库连接。4.3 限流的三档做法计数器、单机令牌桶、Redis 分布式限流压测过后你会发现即使秒杀链路设计得再好接口也需要一层限流保护。常见的从业方案分三档。第一档是用户维度计数器限流直接放秒杀接口前面防止脚本一秒刷 100 次。public boolean tryAcquire(String userId) { // key 精确到秒同一用户在一秒内最多通过 10 次 String key seckill:rate: userId : System.currentTimeMillis() / 1000; Long count stringRedisTemplate.opsForValue().increment(key); if (count ! null count 1L) { stringRedisTemplate.expire(key, Duration.ofSeconds(1)); } return count ! null count 10; }第二档是单机令牌桶用 Guava 的RateLimiter适合单节点部署// 每秒生成 500 个令牌接口获取不到令牌就直接拒绝 RateLimiter limiter RateLimiter.create(500); if (!limiter.tryAcquire()) { throw new BizException(系统繁忙请稍后再试); }第三档是 Redis Lua 脚本做分布式限流多个节点共用同一个计数适合项目里明确写了“多实例部署”的加分项。Lua 脚本的核心逻辑还是INCR EXPIRE和第一档相同只是把原子性放到 Redis 端保证。限流的阈值怎么定先看压测结果如果你的环境单机能扛 2000 QPS限流阈值设 500 就是给自己留了 4 倍安全余量确保突发流量不会把系统打穿。RateLimiter的 500 是平均速率默认不允许瞬时突发秒杀开启瞬间会有大量请求被拒这是正常现象。拒绝策略不是报错而是返回一个友好的“系统繁忙”配合幂等锁保证用户刷新后还能正常参与。5. 秒杀项目最容易踩的 5 个坑从超卖到消息重复消费5.1 库存扣成负数UPDATE 语句缺少 stock 0 条件现象压测结束后查数据库seckill_stock变成了 -23。前端页面明明显示已抢光后台还在不断生成订单。原因扣减 SQL 只写了SET seckill_stock seckill_stock - 1没有加AND seckill_stock 0。第一个请求把库存从 1 扣到 0第二个请求继续从 0 扣到 -1并发越高负得越多。Redis 预扣和数据库扣减两层都必须带这个条件。解决SQL 改成WHERE id ? AND seckill_stock 0并检查返回行数为 0 说明没库存。如果已经出现负数先用一条修正 SQL 把库存归零再人工核对订单谁也不能保证负数期间超卖的那些订单能全部正常履约。这个坑属于写代码时最容易遗漏、演示时最容易暴露的问题建议在代码审查清单里单独列一条。5.2 Redis 有库存、数据库没库存消费失败的补偿现象Redis 显示还有 12 件库存用户也能抢到但“我的订单”列表里就是查不到订单活动结束时数据库库存还剩 12 件没卖出去。原因预扣库存成功但消息没送达消费者。常见原因有三个RabbitMQ 消费者抛了异常没做重试、消息发送时交换机或路由配置错误、消费者停机期间消息积压没有触发告警。Redis 预扣和数据库扣减之间隔着一个消息队列这个中间环节不是 100% 可靠的。解决发送消息时设置mandatory加回调发送失败立即把 Redis 库存回补并直接返回“活动太火爆”。消费端给onMessage加 try-catch业务异常记录日志后返回ack避免消息无限重回队列导致死循环真正无法处理的进入死信队列由人工或补偿任务处理。最后再放一个定时对账任务每隔几分钟比对 Redis 库存和数据库库存发现差异超过阈值就告警。注意 Redis 和数据库会短暂不一致对账要容忍 1 到 2 分钟的时间窗口。5.3 防重复下单失效setIfAbsent 的返回值被忽略现象同一个用户开 5 个线程同时点击“立即抢购”最终生成了 3 条订单。用日志排查幂等判断代码执行了但结果没生效。原因幂等判断用的是“先查订单表是否已存在不存在再插入”两个线程同时查到不存在同时插入数据库唯一索引uk_user_goods拦住了其中一条但另外几条是在不同时间戳下进的插进去了。有些实现把 Redis 的setIfAbsent返回值当成普通布尔值用没有做空值判断Redis 连接异常时返回 null自动拆箱直接抛 NPE请求直接报错而不是被拦住。解决用 Redis 的setIfAbsent做第一层数据库唯一索引做第二层两层只要有一层生效就不会出现重复订单。同时注意setIfAbsent返回值必须用Boolean.TRUE.equals()判断把它当成“可能为 null”的对象处理。插入订单时捕获DuplicateKeyException捕获到就按重复抢处理返回“你已经参与过秒杀”而不是让用户看到 500 页面。这个组合能覆盖 Redis 宕机和并发穿透两种极端情况。5.4 SpringBoot 版本太高导致 Redis 配置出问题现象照着教程在application.yml里写spring.redis.host项目启动直接报无法解析配置或者把 SpringBoot 升级到 3.x 后原来的RedisTemplate注入正常但项目跑起来一直连不上 Redis。原因SpringBoot 2.x 到 3.x 的改动非常大不只是版本号变了。3.x 基于 Java 17 和 Jakarta EE很多旧写法的配置项失效Redis 连接工厂的内部实现也做了调整。网上大量教程和毕设源码是基于 SpringBoot 2.x 写的直接套到新版本上必然翻车。这类问题排查起来很耗时间而且往往不是你的业务代码出错是框架底层行为变了。解决做毕设、课设和实训场景建议锁 SpringBoot 2.7.18这是 2.x 的最后一个版本兼容性最好网上资料也最多。用spring-boot-starter-data-redis搭配 Lettuce不要自己手写连接工厂和序列化配置默认的StringRedisTemplate和RedisTemplate足够覆盖秒杀场景。一句血泪经验不要在演示前一两天升级 SpringBoot 版本项目跑得好好的就别动它版本升级属于那种“换完就后悔”的操作。5.5 RabbitMQ 消息重复投递导致重复订单现象消费者服务重启过一次后数据库里出现了两条一模一样的订单连order_no都相同。查看 RabbitMQ 管理后台消息并没有被消费掉而是重新进入了队列。原因消费者在“插入订单”成功之后、向 RabbitMQ 发送 ack 之前宕机消息会被判定为未消费重新投递给其他消费者。RabbitListener默认是自动 ack方法执行没抛异常就确认但 JVM 死在确认前消息还是会回来。这不是框架 bug是消息队列“至少一次投递”的固有语义消费端必须设计成幂等的。解决数据库的uk_order_no唯一索引已经给了你最后一道防线插入时重复会抛DuplicateKeyException捕获后直接返回成功不要抛出去。把消费者的 ack 模式改成手动MANUAL处理完业务后再确认能减少重复窗口但手动 ack 一旦忘记确认消息会一直积压调试成本高。给消费者加上业务日志记录orderNo和消费结果排查时能直接从日志看出哪条消息被消费了几次。重复消费是消息队列的常态不是异常这句话可以写进项目文档。6. 交付前的最后验证库存对账脚本、Docker 部署与答辩追问6.1 用脚本核对 Redis 库存与数据库库存演示前一天我会跑一遍对账脚本确认 Redis 预扣库存和数据库真实库存之间没有不可解释的差异。脚本不复杂Python 几行就能完成。import redis import pymysql r redis.Redis(hostlocalhost, port6379, db0) db pymysql.connect(hostlocalhost, userroot, password123456, databaseseckill, charsetutf8mb4) with db.cursor() as cursor: cursor.execute(SELECT id, seckill_stock FROM seckill_goods) goods_rows cursor.fetchall() for goods_id, db_stock in goods_rows: r_stock r.get(fseckill:stock:{goods_id}) if r_stock is None: print(f警告: goods{goods_id} 在 Redis 中无库存 key) elif int(r_stock) db_stock: print(f不一致: goods{goods_id}, redis{int(r_stock)}, db{db_stock})对账脚本的判定逻辑要注意Redis 库存和数据库库存正常情况是“Redis 小于等于数据库”因为预扣先发生在 Redis数据库随后才扣。如果 Redis 比数据库大说明有预扣后没落库的消息需要去 RabbitMQ 看死信队列如果 Redis 比数据库小很多说明有库存被扣了但订单没创建可能有人恶意刷接口把库存扣没了。演示前跑一遍这个脚本Redis 和数据库的差异能在两分钟之内暴露问题省得现场演示时翻车。6.2 演示参数与 Docker 部署秒杀开始时间不要写死在代码里配置化是最省事的做法。在application.yml里定义seckill.start-time和seckill.end-time用ConfigurationProperties读取演示时可以临时调整时间范围不用重新编译。我见过有人把时间写死在 SQL 初始化数据里演示时活动没开始全场对着“不在秒杀时间段内”干瞪眼。部署到演示环境常见做法是把 SpringBoot 应用打成 jar用 Docker 跑起来MySQL 和 Redis 用 docker-compose 一并编排。FROM eclipse-temurin:17-jre WORKDIR /app COPY target/seckill.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar, --spring.profiles.activeprod]docker build -t seckill-demo . docker run -d -p 8080:8080 --name seckill \ --network seckill-net \ -e SPRING_PROFILES_ACTIVEprod \ seckill-demoDocker 部署要提前验证两件事。一是 MySQL、Redis、RabbitMQ 三个容器是否在同一个自定义网络里容器之间用服务名访问而不是localhost二是时区问题容器默认是 UTC 时间秒杀活动时间基于中国时区的话数据库和 JVM 时区不一致会导致活动时间判断完全错乱启动参数里加-Duser.timezoneAsia/ShanghaiMySQL 连接串加serverTimezoneAsia/Shanghai。这个坑在演示当天早上出现的概率极高提前一天用 Docker 跑一遍完整流程能省掉很多尴尬。6.3 答辩和面试的提问链路三问三答答辩和面试官对秒杀项目的高频追问集中在三个问题上。第一问超卖怎么防标准回答是三层防线——RedisDECR原子预扣解决用户请求层的并发数据库UPDATE ... WHERE stock 0解决最终落库的并发唯一索引解决重复下单。不要只说一层说三层才能体现系统性。第二问接口怎么防刷回答用户维度幂等setIfAbsent加每秒限流能主动提“计数器限流和令牌桶的差别”就很加分计数器适合用户维度防刷令牌桶适合全局流量整形。第三问Redis 和数据库库存不一致怎么办回答消费失败补偿、死信队列、定时对账脚本三层兜底顺便可以说出对账脚本容忍秒级时间窗口这个细节证明你真正跑过压测、见过真实数据。最后分享一个我自己的教训。曾经演示前一天改了 RabbitMQ 的队列配置只加了一个死信交换机结果消费者监听注解里的队列名没改启动时静默失败秒杀流程一直卡在“排队中”Redis 库存扣了一大半订单一条没生成。后来排查到是队列绑定关系对不上回滚配置、重建队列、重跑对账脚本才救回来。从那以后我养成了一个习惯演示前只验证不修改要改配置就备份原文件出了问题能一条命令回滚。希望这个教训能帮你避开同样的坑也希望能帮到你把这套秒杀项目真正跑成一个拿得出手的作品。本文还有配套的精品资源点击获取