第47篇:Java综合实战:电商秒杀系统(高并发场景)
2026/9/13 20:54:45 网站建设 项目流程

专栏系列: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 1

3. 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二级缓存、永久缓存不主动过期。

九、项目总结与企业面试要点

本文实现的秒杀系统完全贴合企业生产高并发场景,核心设计思想:层层过滤、缓存兜底、异步削峰、多重兜底

面试高频提问总结:

  1. 秒杀如何解决超卖?Lua原子操作+乐观锁+分布式锁多重保障

  2. 如何防止用户重复秒杀?Redis记录+数据库唯一索引幂等

  3. 高并发削峰方案?缓存拦截+消息队列异步处理

  4. 秒杀架构分层逻辑?前端拦截-限流-缓存-异步MQ-数据库落地

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询