
简介本资源是一套基于Spring Boot 2.x实现的轻量级Java高并发秒杀系统实战项目面向Java后端初学者与中级开发者聚焦高并发场景下的核心问题解决如库存超卖、请求洪峰应对、数据库压力缓解等。压缩包共66个文件含23个Java业务与配置类涵盖Controller、Service、Mapper及Redis缓存逻辑、5个前端静态资源HTML/CSS/JS用于简易抢购界面展示、5张效果截图show-1.png至show-3.png等直观呈现系统运行状态以及SQL建表与初始化脚本、Maven构建配置pom.xml、mvnw和基础配置文件application.yml、properties整体体积仅2.34MB结构清晰、开箱即用。目前已有163人学习下载读者可直接导入IDE运行完整掌握限流令牌桶、缓存预热、消息队列削峰、验证码校验及数据库乐观锁等关键设计落地细节并通过目录模块src/main/java db static快速理解分层架构与高并发改造路径。1. 秒杀系统不是“高并发”三个字就能糊弄过去的它是一场对Java工程能力的极限压力测试你写完一个带库存扣减的SpringBoot接口本地压测QPS 200就敢叫“秒杀系统”别急——真实场景里10万用户同时点下“立即抢购”后端服务在3秒内要完成限流拦截、用户校验、库存预扣、订单生成、消息落库、超时回滚还要扛住Redis穿透、MySQL行锁争抢、RocketMQ堆积、下游支付回调风暴……这不是堆线程池加缓存就能解决的玄学而是必须把JVM、Spring容器生命周期、数据库事务隔离级别、Redis原子操作、消息中间件可靠性全部串成一条链来设计。本篇不讲“高并发八股文”只聚焦一个可落地、可验证、能上线的最小可行方案用SpringBoot 2.7.x Redis Lua脚本 MySQL乐观锁 RocketMQ事务消息在单机8C16G环境下稳定支撑5000 TPS秒杀请求。适合正在准备Java中高级面试、或接手电商促销模块的工程师——所有代码、配置、压测命令、监控指标都来自我线上灰度环境的真实复刻。2. 为什么不用Synchronized或ReentrantLock从库存扣减看分布式锁的本质缺陷2.1 单机锁在集群环境下彻底失效一个被忽略的致命前提很多人一上来就写synchronized (this) { if (stock 0) { stock--; } }这在单机Tomcat里能跑通但部署到3台机器后每台机器都有自己的stock变量副本。用户A在机器1扣减成功用户B在机器2看到的还是旧库存结果超卖。更隐蔽的是Spring Bean默认是单例this指向的是同一个对象实例但JVM内存不共享锁根本不同步。这不是代码写得不够好而是架构前提错了——秒杀必须默认按分布式场景设计单机锁只能作为本地缓存预校验的辅助手段绝不能承担核心逻辑。2.2 Redis SETNX 过期时间看似简单实则埋着三重坑常见做法是用SET key value NX EX 10实现分布式锁但实际踩过坑才明白坑1锁释放非原子性——先GET判断锁归属再DEL释放中间若服务宕机锁永远不释放死锁坑2锁续期不可靠——用守护线程续期但线程可能被GC停顿卡住导致锁提前过期坑3客户端时钟漂移——不同机器系统时间不一致EX时间计算失准。提示生产环境必须用Redission的RLock它通过Lua脚本保证GETDEL原子性并内置看门狗自动续期。但注意——Redission默认使用org.redisson.codec.JsonJacksonCodec若秒杀商品ID含中文或特殊字符需显式指定StringCodec否则序列化失败导致锁获取为空。2.3 真正可靠的方案Redis Lua脚本原子扣减库存我们放弃“加锁-扣减-释放”三步走直接用Lua脚本在Redis服务端完成原子操作。脚本接收商品ID和扣减数量返回结果码1成功0库存不足-1脚本执行异常-- seckill_stock.lua local stockKey KEYS[1] local buyCount tonumber(ARGV[1]) local currentStock tonumber(redis.call(GET, stockKey)) if currentStock nil then return -1 elseif currentStock buyCount then return 0 else redis.call(DECRBY, stockKey, buyCount) return 1 endJava调用时关键点使用RedisTemplate.execute()传入脚本KEYS[1]对应商品ID的Redis Key如seckill:stock:1001ARGV[1]为本次购买数量秒杀固定为1必须设置DefaultRedisScriptLong并指定返回类型为Long否则返回值被转成String导致判断失败脚本执行失败时redis.call()抛异常需捕获RedisSystemException并记录日志不能静默吞掉——这是发现Redis连接池耗尽的唯一线索。3. MySQL行级锁怎么选乐观锁不是万能解药悲观锁也别乱用3.1 悲观锁SELECT ... FOR UPDATE的适用边界仅限强一致性且低频更新场景秒杀下单时有人主张用SELECT stock FROM product WHERE id ? FOR UPDATE加行锁再UPDATE扣减。这在TPS100时可行但当并发量上来MySQL InnoDB的行锁会升级为间隙锁Gap Lock阻塞范围远超目标行导致大量事务等待锁等待超时默认50秒后抛LockWaitTimeoutException用户看到“系统繁忙”体验极差更致命的是若应用层未正确处理异常事务未rollback锁会持续持有直到连接关闭拖垮整个DB连接池。注意FOR UPDATE必须在事务内执行且该事务不能包含其他无关SQL如查用户信息否则锁持有时间被拉长。我们实测发现加入一句SELECT username FROM user WHERE id ?会让锁平均持有时间增加120ms。3.2 乐观锁的正确打开方式version字段 重试机制但重试次数必须硬限制乐观锁依赖UPDATE product SET stock stock - 1, version version 1 WHERE id ? AND version ?。表面看无锁但问题在于若库存只剩1100个请求同时读到version10099个会因WHERE条件不成立而更新失败若不做重试99%请求直接失败若无限重试CPU空转打满还可能引发雪崩。我们的方案是最多重试3次每次间隔10ms随机抖动避免重试请求扎堆。代码实现Transactional public boolean deductStockOptimistic(Long productId) { int retry 0; while (retry 3) { try { int updated productMapper.deductStockWithVersion(productId); if (updated 1) return true; // 扣减成功 } catch (Exception e) { log.warn(乐观锁扣减失败productId: {}, retry: {}, productId, retry, e); } retry; try { Thread.sleep(10 (long)(Math.random() * 10)); // 10~20ms抖动 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } return false; }3.3 终极方案库存预扣 异步落库用最终一致性换高性能真正高并发场景我们放弃在MySQL里实时扣减库存改为Redis Lua脚本原子扣减预占库存seckill:stock:1001扣减成功后发RocketMQ事务消息到seckill_order_topic消费端异步创建订单、扣减DB真实库存、通知支付系统Redis库存作为“第一道防线”MySQL库存作为“最终确认”两者通过定时任务对账修复。这样MySQL压力降低90%TPS从300提升到4500。但必须接受用户点击“抢购成功”后订单可能因下游失败而取消概率0.02%需前端展示“订单处理中”状态而非立即跳转支付页。4. SpringBoot 2.x里那些被低估的配置细节线程池、事务、Redis连接池4.1 Tomcat线程池不是越大越好合理值CPU核数×2 等待IO时间占比SpringBoot 2.x默认用Tomcat其server.tomcat.max-threads常被设为500甚至1000。但实测发现当线程数200时上下文切换开销剧增CPU利用率反而下降秒杀请求本质是IO密集型Redis/DB/MQ交互应按公式maxThreads CPU核心数 × (1 平均等待时间/平均工作时间)计算我们8C机器实测平均Redis响应15msDB响应25ms业务逻辑5ms总等待时间占比≈(1525)/(15255)89%故maxThreads ≈ 8 × (1 0.89) ≈ 15再加缓冲设为30。配置项server: tomcat: max-threads: 30 min-spare-threads: 10 accept-count: 100 # 队列长度超过30个请求排队4.2 Transactional失效的三大隐性原因代理、异常类型、传播行为秒杀下单方法标注Transactional但常出现事务不回滚原因1自调用失效——orderService.createOrder()内部调用this.deductStock()绕过Spring代理事务不生效。必须改为Autowired OrderService self; self.deductStock()原因2异常被捕获——try-catch吞掉RuntimeExceptionSpring无法感知异常事务不回滚。必须声明throws RuntimeException或throw new RuntimeException(e)原因3传播行为错误——若扣库存方法用PROPAGATION_REQUIRES_NEW会导致扣库存成功但订单创建失败时库存无法回滚。应统一用REQUIRED默认。4.3 Redis连接池参数max-active不是瓶颈max-wait才是雪崩开关spring.redis.jedis.pool.max-active设太大如200会导致Redis服务器连接数超限默认10000但真正压垮服务的是max-wait当连接池耗尽请求在max-wait时间内拿不到连接直接抛JedisConnectionException默认-1无限等待线上必须设为100ms超时立即失败由降级逻辑兜底如返回“库存紧张”同时min-idle设为10避免冷启动时连接重建延迟。spring: redis: jedis: pool: max-active: 50 max-wait: 100ms min-idle: 10 max-idle: 205. 避坑指南线上秒杀系统最常翻车的5个血泪现场5.1 现象压测时Redis QPS飙升到2万但MySQL只有300 TPSCPU 100%原因Redis Lua脚本里用了redis.call(HGETALL, key)遍历哈希表该命令时间复杂度O(N)当商品属性字段超20个时单次脚本执行超5msRedis线程阻塞。解决改用redis.call(HMGET, key, field1, field2)按需取字段或把商品快照存为JSON字符串用GET一次性读取。5.2 现象RocketMQ消费端积压10万条重启后瞬间全量重推原因事务消息的checkLocalTransaction方法未实现或实现中抛出异常导致MQ认为本地事务状态未知持续回查。解决checkLocalTransaction必须返回LocalTransactionState.COMMIT_MESSAGE、ROLLBACK_MESSAGE或UNKNOW且绝不抛异常对UNKNOW状态MQ每60秒回查一次最多查15次超时自动回滚。5.3 现象用户抢购成功但支付页显示“订单不存在”原因RocketMQ消费端处理订单创建时INSERT INTO order成功但UPDATE product SET stock stock - 1因唯一索引冲突失败同一用户重复下单事务回滚订单被删但MQ已ACK。解决订单表加user_idproduct_id联合唯一索引INSERT失败直接拒绝不走后续流程消费端用RocketMQTransactionListener监听事务状态确保最终一致性。5.4 现象凌晨3点秒杀活动开始大量请求500日志显示OutOfMemoryError: Metaspace原因SpringBoot热部署devtools未关闭频繁类加载导致Metaspace溢出或Lombok的Data生成过多getter/setterGC压力大。解决生产环境pom.xml排除spring-boot-devtoolsLombok升级到1.18.30用RequiredArgsConstructor(onConstructor_ __(Autowired))替代Data。5.5 现象Redis缓存击穿热点商品Key过期瞬间涌入2000请求打穿DB原因所有请求共用同一过期时间如EXPIRE seckill:stock:1001 3600Key集体失效。解决设置随机过期时间——EXPIRE seckill:stock:1001 (3600 random(600))或用RedissonBloomFilter预判Key是否存在不存在则加分布式锁重建缓存。6. 验证系统是否真的“扛得住”用Arthas Prometheus做四层压测验证光跑JMeter看TPS是假繁荣。真实可用的秒杀系统必须通过四层验证6.1 第一层JVM层——用Arthas看线程阻塞与GC压力启动Arthas后执行# 查看阻塞线程定位锁竞争 thread -b # 实时监控GC重点关注Old GC频率 dashboard -i 5000 # 抓取慢SQL定位DB瓶颈 trace com.xxx.service.OrderService createOrder --skipJDKMethod false关键指标红线thread -b输出为空无线程阻塞dashboard中Old GC间隔30分钟trace中createOrder方法平均耗时150ms。6.2 第二层中间件层——Prometheus监控Redis与MQ健康度部署PrometheusGrafana采集以下指标指标名健康阈值异常含义redis_connected_clients 500连接数超限需调大maxclientsrocketmq_consumer_offset_lag 100消费延迟检查消费端线程数jvm_memory_used_bytes{areaheap} 70%堆内存使用率过高触发Full GC提示RocketMQ的offset_lag必须用consumerGroup维度聚合避免单个Consumer实例延迟掩盖整体问题。6.3 第三层业务层——用Canal监听MySQL binlog验证数据一致性部署Canal订阅秒杀订单库当Redis扣减1000次MySQL订单表必须新增1000条记录且product.stock减少1000。脚本校验-- 对账SQL每5分钟执行 SELECT (SELECT COUNT(*) FROM seckill_order WHERE status success) as order_count, (SELECT stock FROM product WHERE id 1001) as db_stock, (SELECT CAST(value AS SIGNED) FROM redis_key WHERE key seckill:stock:1001) as redis_stock FROM dual;差值5即告警触发人工介入。6.4 第四层用户体验层——用Chrome DevTools模拟弱网验证首屏加载在Network面板中设置Slow 3G1.6Mbps150ms RTT录制用户点击“抢购”到看到“下单成功”的全过程关键路径必须≤3秒含DNSTCPSSLAPI渲染若超时优先优化前端资源压缩JS/CSS、启用HTTP/2、预加载Redis连接池绝对禁止在按钮点击后弹出“请稍候”遮罩层——这会掩盖真实失败用户反复点击导致超卖。最后说句实在话我上线过3次百万级流量秒杀每次都在活动前48小时推倒重做Redis Lua脚本——因为上一次的版本在压测时暴露了redis.call(INCR)在集群模式下的哈希槽迁移问题。技术没有银弹只有把每个环节的“为什么”想透才能在流量洪峰来临时让系统稳如磐石。希望帮到你。本文还有配套的精品资源点击获取