专栏系列: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
sql
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
sql
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配置
java
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. 统一返回结果类
java
@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,保证判断库存、扣减库存、记录用户下单三步原子执行
java
-- 秒杀库存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脚本配置类
java
@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本地限流)
java
@Component
public class RateLimitUtil {
// 每秒放行200个请求
private final RateLimiter rateLimiter = RateLimiter.create(200);
public boolean tryAcquire() {
// 非阻塞获取令牌
return rateLimiter.tryAcquire();
}
}
5. 秒杀核心Service(缓存+Lua+MQ异步)
java
@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消息实体与消费者(异步生成订单)
消息实体
java
@Data
public class SeckillMessage implements Serializable {
private Long userId;
private Long goodsId;
}
MQ配置与消费者
java
@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");
}
}
java
@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层数据库兜底扣库存(乐观锁)
java
@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接口
java
@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-数据库落地