
简介这是一套面向Java后端开发者与高并发系统学习者的商城秒杀实战项目源码聚焦高并发场景下的性能优化与分布式协同问题。项目基于SpringBoot快速构建采用Mybatis实现数据持久层MySQL作为主数据库并深度集成Redis缓存、RabbitMQ异步削峰、ZooKeeper分布式协调及Redisson分布式锁等主流中间件完整覆盖秒杀核心链路库存预减、请求限流、异步下单、超卖防控与结果通知。压缩包共258个文件含48个Java业务与配置类、170个XML映射与配置文件支撑Mybatis与Spring生态、12个JSP前端页面及配套CSS/JS资源整体仅451KB轻量易读结构清晰便于逐模块剖析。目前已有361人下载学习适合中高级开发者深入理解秒杀系统架构设计、中间件协同机制与典型分布式问题解决方案。1. 项目背景与核心挑战为什么秒杀系统这么“难”最近几年我参与和主导过好几个电商秒杀系统的设计与重构。每次和团队聊起这个话题大家的第一反应往往是“不就是做个限时抢购嘛把商品库存扣减一下不就行了” 但真正上手后从第一个用户点击“立即抢购”按钮开始各种问题就会像潮水一样涌来页面瞬间卡死、库存莫名超卖、订单创建失败、数据库连接池被打满…… 一个看似简单的业务背后是对系统架构、并发编程、数据库和中间件技术的综合大考。这个基于 SpringBoot Mybatis MySQL 中间件构建的商城秒杀系统源码就是一个典型的、用于学习和理解高并发场景下核心解决方案的实战项目。它不是一个玩具 Demo而是试图模拟真实秒杀场景中会遇到的关键问题并给出工程化的解决思路。秒杀的核心矛盾在于瞬间涌入的海量请求与有限系统资源尤其是数据库之间的巨大冲突。想象一下一万个用户同时抢购100件商品如果这一万个请求都毫无阻拦地直达数据库去执行UPDATE stock SET stock stock - 1 WHERE id ? AND stock 0那么数据库的 CPU、IO 和连接数瞬间就会成为瓶颈导致绝大多数请求超时用户体验极差甚至可能引发雪崩拖垮整个商城服务。因此一个合格的秒杀系统其设计目标绝不是“实现功能”而是“在高并发下稳定、正确、高性能地实现功能”。它需要解决几个核心挑战瞬时高并发流量的削峰与限流、库存扣减的绝对准确性防超卖、系统的高可用与可扩展性以及防止各种恶意请求如脚本、刷单。这个项目源码正是围绕这些挑战利用 SpringBoot 的快速开发能力整合一系列中间件来构建防御工事。接下来我会结合这个项目的常见实现拆解每一个技术选型背后的“为什么”并分享在实际部署和压测中容易踩到的坑。2. 技术栈选型深度解析每一层都不是随便选的看到 SpringBoot Mybatis MySQL 中间件这个组合很多初学者可能会觉得这是 Java 后端的“标配”没什么特别的。但恰恰是这些“标配”组件在秒杀场景下的特定用法和调优决定了系统的生死。我们来逐一拆解。2.1 SpringBoot快速构建与配置中心化为什么是 SpringBoot 而不是传统的 SSM 手动整合在秒杀这种需要快速迭代、频繁压测调优的场景下效率就是生命。SpringBoot 的自动配置和 Starter 机制让我们能快速引入 Redis、RabbitMQ、Sentinel 等中间件的依赖并几乎零配置地启动一个可用的服务。这对于搭建秒杀系统的原型、进行技术验证至关重要。但 SpringBoot 在这里不只是为了“快”。它的核心价值在于“配置中心化”和“内嵌容器”。秒杀系统往往需要根据压测结果动态调整线程池、连接池、超时时间等大量参数。SpringBoot 的application.yml文件配合ConfigurationProperties可以让我们将所有关键配置集中管理并通过RefreshScope结合配置中心如 Nacos、Apollo实现不停机动态刷新。比如发现数据库连接池不够我们可以立刻在配置中心将spring.datasource.hikari.maximum-pool-size调大而无需重启服务这对在线系统的稳定性是极大的保障。另一个容易被忽略的点是 SpringBoot Actuator。它提供的/actuator/metrics,/actuator/health端点是监控系统健康状态、收集 JVM 和业务指标如秒杀接口 QPS、平均响应时间的利器。没有监控优化就无从谈起。2.2 MyBatis轻量ORM与SQL的绝对掌控在秒杀系统中数据库操作是性能瓶颈的重灾区因此对 SQL 的执行必须有绝对的掌控力。这就是选择 MyBatis 而非 JPA/Hibernate 的主要原因。JPA 的抽象层在复杂业务下很好用但在需要极致性能、编写复杂优化 SQL特别是涉及行锁、乐观锁的场景下其自动生成的 SQL 可能不是最优的而且调试起来更复杂。MyBatis 允许我们直接编写和优化每一条 SQL。例如扣减库存的核心 SQL我们可能会这样写update idreduceStock UPDATE seckill_goods SET stock stock - 1, version version 1 WHERE id #{id} AND stock 0 AND version #{version} /update这条 SQL 结合了“库存大于0”的条件和“乐观锁版本号”机制是防止超卖的经典写法。用 MyBatis我们可以清晰地看到并确保这条 SQL 被精确执行。同时MyBatis 的插件机制Interceptor也非常有用我们可以开发一个插件对所有执行时间过长的 SQL 进行监控告警这在排查性能问题时能救命。关于网络热词中提到的mybatis中的#和$的区别这里必须强调在秒杀系统里必须使用#{}预编译占位符绝对禁止使用${}进行字符串拼接。因为${}会导致 SQL 注入风险而且每次都会生成新的 SQL 语句无法利用数据库的预编译缓存在高并发下会严重拖慢数据库。#{}则能有效防止 SQL 注入并提升性能。2.3 MySQL关系型数据库的坚守与优化很多人会问“秒杀这种高并发场景为什么不直接用 Redis 之类的内存数据库” 这是一个很好的问题。Redis 确实快但它的事务性、持久化机制和复杂查询能力相比 MySQL 仍有不足。在电商系统中库存、订单、用户账户余额等都是强一致性的核心数据最终必须落地到可靠的关系型数据库中。所以常见的架构是“Redis 做前置校验和缓存MySQL 做最终持久化”。MySQL 在这一架构中扮演着“最终防线”和“数据真理源”的角色。它的优化至关重要表结构设计秒杀商品表必须有独立的库存字段并且最好与主商品表分离避免秒杀活动影响正常商品查询。字段要精简索引要高效。通常会在商品ID和活动时间上建立联合索引。事务与隔离级别库存扣减和订单创建必须在同一个事务中。MySQL 默认的 REPEATABLE READ 隔离级别在并发更新时可能会带来较多的锁竞争和死锁风险。对于一些可以接受“秒杀扣减”短暂不一致后续通过异步对账补偿的场景可以考虑使用 READ COMMITTED 级别能减少间隙锁提升并发度。但这需要业务层的精密设计不能一概而论。连接池与慢查询必须使用如 HikariCP 这样的高性能连接池并合理设置maximumPoolSize不是越大越好需根据压测调整。同时一定要开启 MySQL 的慢查询日志定期分析并优化执行缓慢的 SQL。2.4 “中间件”群像构建系统护城河这里的“中间件”是一个集合概念是秒杀系统的灵魂。它们各自承担着不同的削峰、限流、解耦和缓存职责。Redis缓存与计数器的核心。它的作用是多方面的库存预热活动开始前将商品库存从 MySQL 加载到 Redis如String类型的seckill:stock:{goodsId}。后续的库存扣减预检查都在 Redis 中进行压力不会直接到数据库。分布式锁防止用户重复下单。可以使用SET key value NX PX timeout命令实现简单的分布式锁确保一个用户在同一秒杀活动中只能下一个订单。缓存商品详情秒杀页面信息静态化商品详情等读多写少的数据缓存到 Redis减少数据库查询。限流与黑名单使用 Redis 的INCR命令和过期时间可以实现简单的 IP 或用户维度的访问频率限制。消息队列如 RabbitMQ/RocketMQ/Kafka流量削峰与异步解耦。这是秒杀系统最关键的削峰组件。核心流程是用户请求通过 Redis 库存预检后不是直接写数据库而是向消息队列发送一条“秒杀消息”。消息队列作为缓冲区后端有多个消费者服务异步地从队列中取出消息执行真正的库存扣减和订单创建。这样无论前端流量多高后端的数据库处理速度都能保持在一个平稳的水平实现了流量的“削峰填谷”。选择 RabbitMQ 可能更看重其消息可靠性选择 Kafka 则更看重其超高吞吐量。限流降级组件如 Sentinel/Sentinel系统的紧急制动阀。即使有队列削峰也不能让无限制的请求涌入系统。需要在网关或服务入口设置限流规则例如每秒只允许 10000 个请求进入秒杀流程超出部分直接返回“活动太火爆请稍后再试”。同时要对依赖的资源如 MySQL、Redis设置熔断降级规则当这些资源不稳定时快速失败避免线程池被拖垮。3. 秒杀核心流程拆解与实战代码剖析理解了技术选型我们来看一个典型的秒杀请求是如何在这个架构中流转的。这个过程可以分为几个层次前端静态化与请求优化、网关层拦截、服务层逻辑、异步下单队列。3.1 请求链路全景与前端优化用户看到的秒杀页面绝不应该是一个需要实时从后端渲染的动态页面。最佳实践是全页面静态化。在活动开始前将商品详情、活动规则等生成静态 HTML 文件推送到 CDN。用户访问时直接从最近的 CDN 节点获取页面速度极快且对后端零压力。“立即抢购”按钮也需要防抖和计数。前端可以做一个简单的倒计时和状态禁用防止用户疯狂点击。更高级的做法是在倒计时结束时前端并不直接发送请求而是先向一个独立的“计数服务”请求一个动态的、一次性的令牌Token只有拿到令牌的请求才有资格向后端发送秒杀请求。这个令牌可以通过 Redis 生成并严格控制总量这是在前端进行的第一道流量控制。3.2 网关层统一的守卫者所有请求首先到达 API 网关如 Spring Cloud Gateway, Nginx Lua。在这里我们可以做很多事情恶意请求过滤检查 User-Agent拦截明显的脚本工具。限流针对秒杀接口的 URL 路径实施全局 QPS 限流。例如使用 Sentinel 的网关流控规则。参数校验与清洗对请求参数进行基础校验防止无效请求进入业务系统。路由与负载均衡将请求分发到后端的多个秒杀服务实例。3.3 服务层核心逻辑从校验到发券请求通过网关后进入我们的 SpringBoot 服务。这里是业务逻辑的核心区代码必须高效且严谨。Service public class SeckillServiceImpl implements SeckillService { Autowired private RedisTemplateString, String redisTemplate; Autowired private RabbitTemplate rabbitTemplate; Autowired private SeckillGoodsMapper goodsMapper; Override Transactional(rollbackFor Exception.class) public SeckillResponse doSeckill(SeckillRequest request) { Long userId request.getUserId(); Long goodsId request.getGoodsId(); // 1. 校验用户资格是否黑名单、是否已参与 String userKey seckill:user: goodsId : userId; Boolean isMember redisTemplate.opsForSet().isMember(seckill:blacklist, userId.toString()); if (Boolean.TRUE.equals(isMember)) { return SeckillResponse.fail(请勿重复参与或操作异常); } // 2. Redis预减库存核心 Long stock redisTemplate.opsForValue().decrement(seckill:stock: goodsId); if (stock null || stock 0) { // 库存不足需要把刚才减掉的库存加回来补偿 redisTemplate.opsForValue().increment(seckill:stock: goodsId); return SeckillResponse.fail(商品已售罄); } // 3. 获取分布式锁防止同一用户并发请求可选根据情况 String lockKey seckill:lock: goodsId : userId; String lockValue UUID.randomUUID().toString(); Boolean lockAcquired redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS); if (Boolean.FALSE.equals(lockAcquired)) { // 获取锁失败说明该用户正在处理中回滚库存 redisTemplate.opsForValue().increment(seckill:stock: goodsId); return SeckillResponse.fail(请求过于频繁请稍后再试); } try { // 4. 生成秒杀资格Token并放入消息队列 String seckillToken generateToken(userId, goodsId); SeckillMessage message new SeckillMessage(userId, goodsId, seckillToken); // 发送到消息队列进行异步下单处理 rabbitTemplate.convertAndSend(seckill.exchange, seckill.order, message); // 5. 标记用户已参与 redisTemplate.opsForSet().add(userKey, 1); redisTemplate.expire(userKey, 2, TimeUnit.HOURS); // 活动结束后一段时间清理 return SeckillResponse.success(seckillToken, 抢购成功正在生成订单...); } finally { // 释放分布式锁使用Lua脚本保证原子性 String luaScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(luaScript, Long.class), Collections.singletonList(lockKey), lockValue); } } }代码关键点解析预减库存使用 Redis 的DECR命令它是原子操作能保证在高并发下库存计数的准确性。如果减后库存小于0说明已售罄需要立即将库存加回INCR这是一个“补偿”操作保证数据最终一致。分布式锁这里用 Redis 实现的分布式锁是为了防止同一个用户在极端网络情况下发送了多个请求都通过了库存检查。锁的粒度是用户商品持有时间很短仅覆盖生成 Token 和发送消息的过程。务必使用 Lua 脚本释放锁保证判断锁归属和删除操作的原子性这是避免锁误删的经典做法。异步化最耗时的数据库操作扣减真实库存、创建订单被放入消息队列由消费者异步处理。至此用户的本次请求核心处理已经结束响应时间极短通常在几十毫秒内。3.4 异步下单消费者最终一致性的保障消息队列的消费者服务负责最终的“脏活累活”。Component Slf4j public class SeckillOrderConsumer { Autowired private SeckillGoodsMapper goodsMapper; Autowired private OrderService orderService; RabbitListener(queues seckill.order.queue) public void handleSeckillOrder(SeckillMessage message) { Long userId message.getUserId(); Long goodsId message.getGoodsId(); String token message.getToken(); // 1. 再次校验Token防止消息被重复消费或伪造 if (!validateToken(userId, goodsId, token)) { log.warn(无效的秒杀令牌: {}, token); return; // 直接丢弃不处理 } // 2. 数据库扣减库存乐观锁 SeckillGoods goods goodsMapper.selectForUpdate(goodsId); // 或用乐观锁 if (goods null || goods.getStock() 0) { log.error(商品库存不足或不存在goodsId: {}, goodsId); // 这里可以触发补偿逻辑如通知用户抢购失败并可能涉及退款等 return; } // 使用乐观锁更新 int updateCount goodsMapper.reduceStockWithOptimisticLock(goodsId, goods.getVersion()); if (updateCount 0) { // 更新失败说明版本号不对已被其他消费者修改可能是并发冲突记录日志并丢弃 log.warn(秒杀商品库存更新冲突goodsId: {}, goodsId); return; } // 3. 创建订单 try { Order order orderService.createSeckillOrder(userId, goodsId); log.info(秒杀订单创建成功订单号: {}, order.getOrderNo()); // 4. 订单创建成功后可以删除Token更新缓存等 deleteToken(token); } catch (Exception e) { log.error(创建秒杀订单失败 userId: {}, goodsId: {}, userId, goodsId, e); // 极其重要的步骤订单创建失败必须回滚库存 // 方案1调用库存回滚接口同步或异步 // 方案2将失败消息放入一个“死信队列”由另一个服务进行库存回滚和失败订单处理 rollbackStock(goodsId); // 注意这里可能产生少卖但保证了数据最终一致性需要通过定期对账来修复。 } } }消费者层的核心考量幂等性消息队列可能传递重复消息Exactly-Once 很难保证所以消费者必须实现幂等。这里的 Token 校验和数据库乐观锁都是实现幂等的手段。最终一致性这是分布式事务的经典问题。我们采用了“本地事务订单 最大努力通知库存回滚”的模式。即先扣库存本地事务再创建订单另一个本地事务如果订单创建失败则尝试回滚库存。这个回滚可能失败所以系统会存在短暂的不一致库存已扣订单没有。这就需要有一个对账系统定期扫描这种状态异常的数据进行人工或自动的修复如补订单或返还库存。错误处理与补偿消费者的错误处理必须健壮。除了记录日志更重要的是要有补偿机制。将处理失败的消息送入死信队列DLQ是一个好实践由专门的补偿服务来处理避免消息丢失。4. 性能压测、监控与常见“深坑”指南代码写完了部署上线这仅仅是开始。没有经过严格压测和监控的秒杀系统就像没经过测试的火箭升空即爆炸。这部分分享我踩过的坑和总结的经验。4.1 压测模拟真实流量暴露系统短板不要用 JMeter 随便发点请求就完事了。压测需要模拟真实场景。构造真实数据准备至少大于库存数量一个数量级的用户 Token 或 Cookie。模拟时间轴在活动开始前有预热请求开始瞬间有洪峰洪峰过后有持续请求。使用 JMeter 的Synchronizing Timer可以模拟“同时并发”。关注核心指标应用层QPS每秒查询率、RT响应时间、错误率。关注99线和999线的 RT它们代表绝大多数用户的体验。中间件层Redis 的 CPU、内存、连接数、命令耗时RabbitMQ 的队列堆积情况、消费者数量数据库的 CPU、IOPS、活跃连接数、慢查询。系统层服务器的 CPU、内存、网络带宽、磁盘 IO。一个典型的压测暴露的问题在一次压测中我发现 QPS 刚到 2000RT 就飙升到数秒错误率大增。排查过程查看应用监控发现线程池被打满大量请求在等待。检查数据库监控发现 CPU 不高但活跃连接数接近最大值。检查代码发现有一处查询用了SELECT *且没有走索引导致单个查询慢连接被长时间占用。优化 SQL增加索引同时适当调大数据库连接池并监控是否有效。问题解决。4.2 监控告警系统的眼睛和耳朵“无监控不运维”。必须建立完善的监控体系。业务监控秒杀活动关键指标大盘。包括总访问量、总下单量、库存剩余、下单成功率、各环节网关、服务、队列、DB的耗时分布。系统与中间件监控上述压测中关注的各项指标都需要设置告警阈值。例如Redis 内存使用率 80%MySQL 活跃连接数 连接池的 90%都需要立即告警。链路追踪集成 SkyWalking 或 Zipkin当一个用户请求变慢时可以快速定位是卡在网关、业务服务、Redis 查询还是数据库 SQL 上。4.3 常见“深坑”与填坑方案超卖问题这是秒杀的灵魂问题。解决方案是“Redis原子计数 数据库乐观锁”双重保障。Redis 预减做第一层快速拦截数据库乐观锁做第二层最终确认。任何一层失败都立即返回失败。绝对不能在应用层用if (stock 0) { update stock... }这样的非原子操作。Redis 缓存穿透恶意请求查询一个不存在的商品 ID绕过 Redis因为查不到直接击穿到数据库。解决方案布隆过滤器Bloom Filter。在 Redis 里维护一个所有有效商品 ID 的布隆过滤器查询前先过布隆过滤器如果判断不存在直接返回。Redis 缓存击穿某个热点 key如秒杀商品库存在缓存过期的瞬间有大量请求同时来查询全部打到数据库。解决方案永不过期 逻辑过期。物理上不设置过期时间但在 value 中存一个逻辑过期时间。业务线程发现逻辑过期则获取一个分布式锁只有一个线程去数据库加载新数据其他线程等待或返回旧数据。消息队列积压消费者处理速度跟不上生产速度导致队列消息堆积下单延迟越来越长。解决方案增加消费者实例水平扩展。优化消费者逻辑比如批量处理消息。监控队列长度设置告警。积压严重时可以考虑临时降级比如关闭非核心功能保障秒杀核心链路。数据库连接池耗尽这是压测时最常见的问题之一。除了优化慢 SQL还要合理设置连接池参数。HikariCP 的maximumPoolSize不是越大越好设置过大会导致数据库线程上下文切换开销巨大。一个经验公式是连接数 ≈ (核心数 * 2) 有效磁盘数。对于 IO 密集型的数据库操作可以稍大一些但一定要通过压测找到最佳值。前端时间同步问题用户端的时间不准导致有人提前点击按钮。解决方案服务器时间同步。前端倒计时结束后向服务器请求一个“活动开始时间戳”或直接请求动态的秒杀接口地址以后端时间为准。5. 进阶思考从“能用”到“好用”的优化之路当系统能稳定扛住秒杀洪峰后我们可以考虑一些更深入的优化提升系统的弹性、可观测性和成本效益。5.1 服务治理与弹性伸缩在云原生环境下我们可以利用 Kubernetes 的 HPA水平自动伸缩能力。根据 CPU 使用率或自定义指标如消息队列堆积长度自动增加或减少秒杀服务的 Pod 实例。在活动开始前提前扩容活动结束后自动缩容既能应对流量高峰又能节约成本。同时需要完善服务的“无损下线”和“优雅启动”机制。在服务重启或发布时要确保正在处理的秒杀请求不会丢失。SpringBoot 的SmartLifecycle接口和 Kubernetes 的preStop钩子可以配合使用让服务在收到终止信号后先停止接收新请求等待存量请求处理完毕再退出。5.2 数据一致性对账系统如前所述异步下单模式存在数据最终一致性问题。必须建立一个“对账系统”它像系统的审计员定期如每分钟执行以下任务扫描 Redis 中标记为“已抢到资格”但超过一定时间如10分钟仍未生成订单的记录。扫描数据库中库存已扣减但对应订单状态为“失败”或“不存在”的记录。对这些异常记录进行补偿处理可能是重新尝试创建订单也可能是将库存回滚并通知用户。对账系统是保证业务数据最终准确、避免资损的最后一道防线。5.3 容量规划与成本控制秒杀系统通常是“脉冲式”流量为了一年中几次的活动维持庞大的常备集群是巨大的浪费。混合云或 Serverless 是值得考虑的方向。例如将流量入口、静态资源、Redis 缓存等无状态服务部署在公有云上利用其极强的弹性伸缩能力应对洪峰。而核心的数据库、订单处理等有状态服务可以放在自建机房或私有云保证数据安全和长尾流量的稳定性。通过精细化的容量规划和资源调度在保障稳定性的前提下最大化成本效益。回顾整个基于 SpringBoot 的秒杀系统构建过程从技术选型的权衡到核心流程的精细设计再到上线前的压测与监控每一步都充满了权衡与挑战。这套源码提供了一个很好的学习范本但真正应用到生产环境还需要根据自身业务特点、团队技术栈和基础设施情况进行大量的适配和优化。记住没有银弹只有最适合当前场景的解决方案。每一次大促都是对系统架构和团队协作的一次实战演练而事后细致的复盘则是下一次做得更好的基石。本文还有配套的精品资源点击获取