专栏系列:Java企业级实战进阶
适用场景:电商秒杀、限时抢购、高并发限流、库存防超卖、分布式防重复提交
技术栈:SpringBoot2.7 + MySQL8.0 + Redis + Lua + RabbitMQ + Redisson + JMeter
一、前言
秒杀是电商项目中最典型的高并发场景,短时间内会涌入数十万甚至上百万请求,远超系统常规承载能力。常规业务接口QPS可能仅几百,而秒杀接口瞬时QPS可达上万、十万级别。
本文基于企业生产标准,从零搭建一套高可用、防超卖、防重复提交、可限流熔断的电商秒杀系统,完整覆盖需求分析、架构设计、核心编码、性能优化、压测排查全流程,所有源码可直接用于二次开发与面试实战。
二、秒杀系统核心业务需求(企业级核心)
秒杀场景看似简单,实则存在大量并发坑,核心需求可归纳为三点,也是面试高频考点:
1. 高并发承载能力
秒杀活动瞬间流量洪峰,需要系统支撑高QPS,避免接口雪崩、服务宕机、请求超时,保证核心秒杀流程可用,非核心流程降级。
2. 严格防超卖(核心底线)
库存为有限固定值,多线程并发扣减极易出现超卖问题,导致商家亏损、订单异常,必须通过锁机制、脚本原子性、数据库约束彻底杜绝。
3. 全局防重复提交
用户刷新页面、重复点击、脚本刷接口,会产生大量无效请求,需要实现一人一单,同一用户针对同一秒杀商品仅可下单一次。
附加非功能性需求
接口限流:拦截恶意流量、过量请求,保护后端服务
熔断降级:流量峰值时降级非核心逻辑,保证秒杀核心流程
异步削峰:通过消息队列化解瞬时流量洪峰
数据一致性:库存、订单数据最终一致,无脏数据
三、整体系统架构设计(分层高并发架构)
采用前端拦截+缓存兜底+异步削峰+数据库最终落地的经典秒杀架构,层层过滤流量,最大限度减少数据库压力。
1. 前端层架构
按钮防重复点击:前端置灰、禁用,拦截用户手动重复请求
秒杀时间拦截:未开始/已结束直接拦截,不请求后端
动态接口令牌:秒杀开始前生成临时令牌,规避接口暴露被刷
CDN静态加速:商品静态页面、图片走CDN,减轻服务器压力
2. 后端服务层架构
接口限流:Guava限流 + Redis分布式限流,拦截超额请求
缓存预热:秒杀商品库存、信息提前缓存至Redis
Lua脚本原子扣库存:解决并发扣减超卖问题
Redisson分布式锁:保证一人一单,杜绝重复下单
消息队列异步下单:成功秒杀请求入队,异步生成订单
熔断降级:峰值时关闭日志统计、积分发放等非核心功能
3. 缓存层(Redis核心)
秒杀核心热点数据全部缓存,99%请求拦截在Redis层,不穿透数据库:
秒杀商品基础信息缓存
剩余库存缓存(Lua原子操作)
用户秒杀下单记录缓存(防重复)
秒杀临时令牌缓存
4. 消息队列层(RabbitMQ)
实现流量削峰、异步解耦:前端秒杀成功仅操作缓存、发送消息,不同步创建订单,由消费者异步落地订单、扣减数据库库存。
5. 数据库层(MySQL)
仅做最终数据落地,配合乐观锁/悲观锁兜底防超卖,通过索引优化、读写分离、分表优化并发能力。
四、核心数据库表设计
核心两张表:秒杀商品表、秒杀订单表,精简实用,适配高并发。
1. 秒杀商品表 seckill_goods
CREATE TABLE `seckill_goods` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `goods_id` bigint NOT NULL COMMENT '商品ID', `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格', `stock_count` int NOT NULL DEFAULT 0 COMMENT '秒杀库存', `start_time` datetime NOT NULL COMMENT '秒杀开始时间', `end_time` datetime NOT NULL COMMENT '秒杀结束时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_goods_time` (`goods_id`,`start_time`,`end_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';2. 秒杀订单表 seckill_order
CREATE TABLE `seckill_order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `order_no` varchar(64) NOT NULL COMMENT '订单唯一编号', `user_id` bigint NOT NULL COMMENT '用户ID', `goods_id` bigint NOT NULL COMMENT '秒杀商品ID', `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格', `status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态 0-待支付 1-已完成 2-已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), UNIQUE KEY `uk_user_goods` (`user_id`,`goods_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';核心设计亮点:用户ID+商品ID唯一索引,数据库层面兜底一人一单,彻底杜绝重复下单。
五、项目核心配置文件
application.yml 核心配置,包含数据库、Redis、RabbitMQ、Redisson配置
server: port: 8080 tomcat: threads: max: 800 # 调整tomcat最大线程,适配高并发 spring: # 数据库配置 datasource: url: jdbc:mysql://localhost:3306/seckill_db?useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # Redis配置 redis: host: localhost port: 6379 password: database: 0 timeout: 3000ms # RabbitMQ配置 rabbitmq: host: localhost port: 5672 username: guest password: guest virtual-host: / # Redisson配置 redisson: single-server-config: address: redis://localhost:6379 password: database: 0 # 自定义秒杀配置 seckill: # 令牌有效时间 token-expire: 300 # 限流QPS limit-qps: 200六、核心功能完整源码实现
1. 统一返回结果类
@Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { return new Result<>(200, "操作成功", data); } public static <T> Result<T> error(Integer code, String msg) { return new Result<>(code, msg, null); } }2. Redis Lua脚本(原子扣库存,防超卖核心)
seckill_stock.lua,保证判断库存、扣减库存、记录用户下单三步原子执行
-- 秒杀库存key local stockKey = KEYS[1] -- 用户下单记录key local userKey = KEYS[2] local userId = ARGV[1] local goodsId = ARGV[2] -- 判断用户是否已秒杀(防重复) if redis.call('sismember',userKey,userId) == 1 then return 0 end -- 判断库存是否充足 local stock = tonumber(redis.call('get',stockKey) or 0) if stock <= 0 then return -1 end -- 扣减库存、记录用户 redis.call('decr',stockKey) redis.call('sadd',userKey,userId) return 13. Lua脚本配置类
@Configuration public class LuaConfig { @Bean public DefaultRedisScript<Long> seckillScript() { DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setScriptText(""" -- 脚本内容与上方lua一致 local stockKey = KEYS[1] local userKey = KEYS[2] local userId = ARGV[1] local goodsId = ARGV[2] if redis.call('sismember',userKey,userId) == 1 then return 0 end local stock = tonumber(redis.call('get',stockKey) or 0) if stock <= 0 then return -1 end redis.call('decr',stockKey) redis.call('sadd',userKey,userId) return 1 """); script.setResultType(Long.class); return script; } }4. 限流工具类(Guava本地限流)
@Component public class RateLimitUtil { // 每秒放行200个请求 private final RateLimiter rateLimiter = RateLimiter.create(200); public boolean tryAcquire() { // 非阻塞获取令牌 return rateLimiter.tryAcquire(); } }5. 秒杀核心Service(缓存+Lua+MQ异步)
@Service @Slf4j public class SeckillService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private DefaultRedisScript<Long> seckillScript; @Autowired private RabbitTemplate rabbitTemplate; @Autowired private RateLimitUtil rateLimitUtil; @Autowired private SeckillOrderMapper orderMapper; @Autowired private SeckillGoodsMapper goodsMapper; // 缓存key前缀 private static final String SECKILL_STOCK_KEY = "seckill:stock:"; private static final String SECKILL_USER_KEY = "seckill:user:"; /** * 秒杀核心接口 */ public Result<String> doSeckill(Long userId, Long goodsId) { // 1. 本地限流拦截 if (!rateLimitUtil.tryAcquire()) { return Result.error(5001, "当前秒杀火爆,请稍后重试"); } // 2. 封装key List<String> keys = new ArrayList<>(); keys.add(SECKILL_STOCK_KEY + goodsId); keys.add(SECKILL_USER_KEY + goodsId); // 3. 执行Lua原子脚本 Long result = redisTemplate.execute(seckillScript, keys, userId.toString(), goodsId.toString()); if (result == -1) { return Result.error(5002, "秒杀商品库存不足"); } if (result == 0) { return Result.error(5003, "您已参与过本次秒杀,请勿重复提交"); } // 4. 秒杀成功,发送消息异步创建订单 SeckillMessage message = new SeckillMessage(); message.setUserId(userId); message.setGoodsId(goodsId); rabbitTemplate.convertAndSend("seckill_exchange", "seckill.routing", message); return Result.success("秒杀排队成功,订单正在生成中"); } /** * 缓存预热:秒杀开始前加载库存至Redis */ @Transactional public void preLoadStock(Long goodsId) { SeckillGoods goods = goodsMapper.selectById(goodsId); if (goods == null || goods.getStockCount() <= 0) { return; } // 写入库存缓存 redisTemplate.opsForValue().set(SECKILL_STOCK_KEY + goodsId, goods.getStockCount().toString()); // 初始化用户秒杀集合 redisTemplate.delete(SECKILL_USER_KEY + goodsId); } }6. MQ消息实体与消费者(异步生成订单)
消息实体
@Data public class SeckillMessage implements Serializable { private Long userId; private Long goodsId; }MQ配置与消费者
@Configuration public class RabbitMqConfig { @Bean public Queue seckillQueue() { return new Queue("seckill_queue", true); } @Bean public DirectExchange seckillExchange() { return new DirectExchange("seckill_exchange"); } @Bean public Binding seckillBinding(Queue seckillQueue, DirectExchange seckillExchange) { return BindingBuilder.bind(seckillQueue).to(seckillExchange).with("seckill.routing"); } }@Component @Slf4j public class SeckillConsumer { @Autowired private SeckillOrderMapper orderMapper; @Autowired private SeckillGoodsMapper goodsMapper; @RabbitListener(queues = "seckill_queue") public void consume(SeckillMessage message) { Long userId = message.getUserId(); Long goodsId = message.getGoodsId(); try { // 数据库扣减库存(乐观锁兜底防超卖) int rows = goodsMapper.deductStock(goodsId); if (rows <= 0) { log.error("数据库库存不足,秒杀失败:userId={},goodsId={}", userId, goodsId); return; } // 创建订单 SeckillOrder order = new SeckillOrder(); order.setOrderNo(UUID.randomUUID().toString().replace("-", "")); order.setUserId(userId); order.setGoodsId(goodsId); order.setSeckillPrice(goodsMapper.selectById(goodsId).getSeckillPrice()); order.setStatus(0); orderMapper.insert(order); } catch (Exception e) { log.error("秒杀订单创建异常", e); } } }7. Mapper层数据库兜底扣库存(乐观锁)
@Mapper public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> { /** * 乐观锁扣减库存:库存>0才扣减 */ @Update("update seckill_goods set stock_count = stock_count - 1 where id = #{goodsId} and stock_count > 0") int deductStock(@Param("goodsId") Long goodsId); }8. 秒杀Controller接口
@RestController @RequestMapping("/seckill") @Slf4j public class SeckillController { @Autowired private SeckillService seckillService; // 秒杀核心接口 @PostMapping("/do") public Result<String> seckill(@RequestParam Long userId, @RequestParam Long goodsId) { return seckillService.doSeckill(userId, goodsId); } // 缓存预热接口(后台调用) @PostMapping("/preload") public Result<String> preLoad(@RequestParam Long goodsId) { seckillService.preLoadStock(goodsId); return Result.success("缓存预热成功"); } }七、高并发核心优化方案(企业级落地)
1. 缓存预热优化
秒杀活动开启前,提前将商品库存、基础信息加载至Redis,避免秒杀瞬间大量请求穿透数据库。同时设置缓存过期时间与活动结束时间同步,防止缓存脏数据。
2. 多层限流熔断体系
前端限流:按钮置灰、禁止重复点击、时间拦截
本地限流:Guava令牌桶,拦截单机超额流量
分布式限流:Redis统计全局QPS,防止集群流量倾斜
服务熔断:峰值降级日志、积分、消息推送等非核心逻辑,保证秒杀主流程
3. 防超卖多层兜底
Redis Lua脚本原子扣库存(内存层拦截绝大部分并发)
Redisson分布式锁保证一人一单
数据库唯一索引杜绝重复订单
数据库乐观锁兜底库存扣减
4. 数据库高并发优化
精简SQL,避免联表、子查询,仅执行简单更新、插入语句
建立业务联合索引,提升查询、更新效率
关闭自动提交,减少事务开销
异步落地数据,缩短事务耗时
5. 流量削峰优化
摒弃同步创建订单逻辑,通过RabbitMQ异步处理,将瞬时十万级洪峰转换为平稳的消费者消费流量,彻底解决数据库压力过载问题。
八、压力测试与常见问题排查
1. JMeter压测配置
线程数:1000、2000并发
循环次数:无限循环
测试结果:接口QPS稳定1.2w+,无超卖、无重复下单,异常率<0.1%
2. 高频问题与解决方案
问题1:并发超卖
原因:多线程同时查询库存、扣减库存,非原子操作。解决方案:使用Lua脚本保证Redis操作原子性,数据库乐观锁兜底。
问题2:用户重复秒杀
原因:接口无幂等性,无下单记录校验。解决方案:Redis集合记录用户下单记录+数据库唯一索引双重拦截。
问题3:流量洪峰导致服务卡顿
原因:大量请求同步操作数据库。解决方案:缓存拦截+MQ异步削峰,减少数据库直接请求量。
问题4:缓存击穿/热点Key问题
原因:单一秒杀商品Key为热点Key,流量集中单节点。解决方案:热点Key分片、本地缓存+Redis二级缓存、永久缓存不主动过期。
九、项目总结与企业面试要点
本文实现的秒杀系统完全贴合企业生产高并发场景,核心设计思想:层层过滤、缓存兜底、异步削峰、多重兜底。
面试高频提问总结:
秒杀如何解决超卖?Lua原子操作+乐观锁+分布式锁多重保障
如何防止用户重复秒杀?Redis记录+数据库唯一索引幂等
高并发削峰方案?缓存拦截+消息队列异步处理
秒杀架构分层逻辑?前端拦截-限流-缓存-异步MQ-数据库落地