仿京东秒杀系统设计与实现
前言
秒杀系统是电商平台中最具挑战性的业务场景之一。每逢618、双11等大促活动,京东、淘宝等平台都会面临数百万甚至上千万的并发请求。本文将从架构设计、核心技术、代码实现等方面,详细讲解如何设计一个高并发、高可用的秒杀系统。
一、秒杀系统的业务特点
1.1 秒杀业务的核心挑战
diff
+------------------+ +------------------+ +------------------+
| 瞬时高并发 | | 库存超卖风险 | | 数据一致性 |
| 100万+ QPS | | 超卖=亏损 | | 缓存与DB同步 |
+------------------+ +------------------+ +------------------+
| | |
v v v
+------------------+ +------------------+ +------------------+
| 服务雪崩 | | 恶意请求 | | 用户体验 |
| 系统崩溃风险 | | 刷单/机器人 | | 响应时间要求 |
+------------------+ +------------------+ +------------------+
1.2 秒杀系统的核心指标
| 指标 | 要求 | 说明 |
|---|---|---|
| QPS | 100万+ | 峰值每秒请求数 |
| 响应时间 | <100ms | 用户可接受的等待时间 |
| 可用性 | 99.99% | 系统稳定性要求 |
| 库存准确性 | 100% | 绝对不能超卖 |
二、系统架构设计
2.1 整体架构图
lua
+------------------+
| 用户请求 |
+--------+---------+
|
+--------v---------+
| CDN |
| 静态资源分发 |
+--------+---------+
|
+--------v---------+
| Nginx集群 |
| 负载均衡/限流 |
+--------+---------+
|
+-----------------------------+-----------------------------+
| | |
+--------v---------+ +--------v---------+ +--------v---------+
| Gateway网关 | | Gateway网关 | | Gateway网关 |
| 认证/限流/路由 | | 认证/限流/路由 | | 认证/限流/路由 |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
+-----------------------------+-----------------------------+
|
+-----------------------------+-----------------------------+
| | |
+--------v---------+ +--------v---------+ +--------v---------+
| 秒杀服务集群 | | 秒杀服务集群 | | 秒杀服务集群 |
| 业务逻辑处理 | | 业务逻辑处理 | | 业务逻辑处理 |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
+-----------------------------+-----------------------------+
|
+------------------+------------------+
| | |
+--------v-------+ +-------v--------+ +------v-------+
| Redis集群 | | RocketMQ集群 | | MySQL集群 |
| 库存/限流 | | 异步削峰 | | 订单存储 |
+----------------+ +----------------+ +--------------+
2.2 核心组件说明
- CDN层:静态页面、图片等资源缓存
- Nginx层:负载均衡、流量控制、静态化
- Gateway层:统一入口、认证鉴权、限流熔断
- 服务层:秒杀核心业务逻辑
- 缓存层:Redis集群,库存预热、分布式锁
- 消息队列:RocketMQ异步处理,削峰填谷
- 数据库层:MySQL主从集群,最终数据持久化
三、数据库设计
3.1 核心表结构
sql
-- 秒杀商品表
CREATE TABLE `seckill_goods` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
`goods_name` VARCHAR(128) NOT NULL COMMENT '商品名称',
`goods_img` VARCHAR(256) DEFAULT NULL COMMENT '商品图片',
`original_price` DECIMAL(10,2) NOT NULL COMMENT '原价',
`seckill_price` DECIMAL(10,2) NOT NULL COMMENT '秒杀价',
`stock_count` INT(11) NOT NULL COMMENT '库存数量',
`start_time` DATETIME NOT NULL COMMENT '秒杀开始时间',
`end_time` DATETIME NOT NULL COMMENT '秒杀结束时间',
`status` TINYINT(1) DEFAULT 0 COMMENT '状态:0-未开始,1-进行中,2-已结束',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_goods_id` (`goods_id`),
KEY `idx_start_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';
-- 秒杀订单表
CREATE TABLE `seckill_order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
`seckill_goods_id` BIGINT(20) NOT NULL COMMENT '秒杀商品ID',
`goods_name` VARCHAR(128) NOT NULL COMMENT '商品名称',
`goods_price` DECIMAL(10,2) NOT NULL COMMENT '商品价格',
`order_status` TINYINT(1) DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_goods` (`user_id`, `seckill_goods_id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';
-- 库存流水表(用于库存对账)
CREATE TABLE `stock_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`seckill_goods_id` BIGINT(20) NOT NULL,
`order_no` VARCHAR(32) NOT NULL,
`stock_change` INT(11) NOT NULL COMMENT '库存变化:负数为扣减',
`type` TINYINT(1) NOT NULL COMMENT '类型:1-扣减,2-回滚',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_seckill_goods_id` (`seckill_goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';
四、核心技术实现
4.1 项目结构
bash
seckill-system/
├── seckill-common/ # 公共模块
│ ├── src/main/java/
│ │ └── com/seckill/common/
│ │ ├── constant/ # 常量定义
│ │ ├── enums/ # 枚举类
│ │ ├── exception/ # 异常定义
│ │ ├── result/ # 统一返回
│ │ └── utils/ # 工具类
├── seckill-gateway/ # 网关服务
├── seckill-service/ # 秒杀服务
│ ├── src/main/java/
│ │ └── com/seckill/service/
│ │ ├── controller/ # 控制器
│ │ ├── service/ # 业务逻辑
│ │ ├── mapper/ # 数据访问
│ │ ├── entity/ # 实体类
│ │ ├── dto/ # 数据传输对象
│ │ ├── config/ # 配置类
│ │ └── mq/ # 消息队列
└── seckill-admin/ # 管理后台
4.2 实体类定义
java
package com.seckill.service.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 秒杀商品实体
*/
@Data
@TableName("seckill_goods")
public class SeckillGoods {
@TableId(type = IdType.AUTO)
private Long id;
private Long goodsId;
private String goodsName;
private String goodsImg;
private BigDecimal originalPrice;
private BigDecimal seckillPrice;
private Integer stockCount;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
java
package com.seckill.service.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 秒杀订单实体
*/
@Data
@TableName("seckill_order")
public class SeckillOrder {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private Long goodsId;
private Long seckillGoodsId;
private String goodsName;
private BigDecimal goodsPrice;
private Integer orderStatus;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
private LocalDateTime payTime;
}
4.3 统一响应结果
java
package com.seckill.common.result;
import lombok.Data;
import java.io.Serializable;
/**
* 统一响应结果
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
private T data;
private Result() {}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> error(ResultCode resultCode) {
return error(resultCode.getCode(), resultCode.getMessage());
}
}
java
package com.seckill.common.result;
import lombok.Getter;
/**
* 响应状态码枚举
*/
@Getter
public enum ResultCode {
SUCCESS(200, "操作成功"),
ERROR(500, "系统错误"),
// 秒杀相关
SECKILL_NOT_START(1001, "秒杀活动未开始"),
SECKILL_END(1002, "秒杀活动已结束"),
SECKILL_STOCK_EMPTY(1003, "商品已售罄"),
SECKILL_REPEAT(1004, "请勿重复秒杀"),
SECKILL_LIMIT(1005, "访问过于频繁,请稍后重试"),
SECKILL_QUEUE(1006, "排队中,请稍后查询结果"),
// 用户相关
USER_NOT_LOGIN(2001, "用户未登录"),
USER_NOT_EXIST(2002, "用户不存在");
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
五、限流与防刷策略
5.1 限流架构
sql
+----------------------+
| 用户请求 |
+----------+-----------+
|
+----------v-----------+
| Nginx限流 |
| limit_req_zone |
+----------+-----------+
|
+----------v-----------+
| Gateway限流 |
| Sentinel/自定义 |
+----------+-----------+
|
+----------v-----------+
| 接口级别限流 |
| @RateLimiter |
+----------+-----------+
|
+----------v-----------+
| 用户级别限流 |
| Redis + Lua |
+----------------------+
5.2 Redis + Lua 实现分布式限流
java
package com.seckill.service.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
/**
* Redis配置类
*/
@Configuration
public class RedisConfig {
/**
* 限流Lua脚本
*/
@Bean
public DefaultRedisScript<Long> rateLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(
new ClassPathResource("lua/rate_limit.lua")));
script.setResultType(Long.class);
return script;
}
/**
* 库存扣减Lua脚本
*/
@Bean
public DefaultRedisScript<Long> stockDeductScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(
new ClassPathResource("lua/stock_deduct.lua")));
script.setResultType(Long.class);
return script;
}
}
rate_limit.lua - 滑动窗口限流脚本:
lua
-- 限流Lua脚本:滑动窗口算法
-- KEYS[1]: 限流的key
-- ARGV[1]: 限流阈值
-- ARGV[2]: 时间窗口(秒)
-- ARGV[3]: 当前时间戳
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 移除时间窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 添加当前请求
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, window)
return 1 -- 允许通过
else
return 0 -- 限流
end
5.3 限流服务实现
java
package com.seckill.service.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
/**
* 限流服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RateLimitService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedisScript<Long> rateLimitScript;
private static final String RATE_LIMIT_PREFIX = "seckill:limit:";
/**
* 检查是否被限流
* @param userId 用户ID
* @param goodsId 商品ID
* @param limit 限流阈值
* @param windowSeconds 时间窗口(秒)
* @return true-允许通过, false-被限流
*/
public boolean tryAcquire(Long userId, Long goodsId, int limit, int windowSeconds) {
String key = RATE_LIMIT_PREFIX + goodsId + ":" + userId;
long now = System.currentTimeMillis();
Long result = redisTemplate.execute(
rateLimitScript,
Collections.singletonList(key),
limit, windowSeconds, now
);
return result != null && result == 1;
}
/**
* IP维度限流
*/
public boolean tryAcquireByIp(String ip, int limit, int windowSeconds) {
String key = RATE_LIMIT_PREFIX + "ip:" + ip;
long now = System.currentTimeMillis();
Long result = redisTemplate.execute(
rateLimitScript,
Collections.singletonList(key),
limit, windowSeconds, now
);
return result != null && result == 1;
}
}
5.4 自定义限流注解
java
package com.seckill.service.annotation;
import java.lang.annotation.*;
/**
* 限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流key前缀
*/
String prefix() default "";
/**
* 限流阈值
*/
int limit() default 10;
/**
* 时间窗口(秒)
*/
int window() default 1;
/**
* 限流维度:USER/IP/GLOBAL
*/
LimitType type() default LimitType.USER;
enum LimitType {
USER, // 用户维度
IP, // IP维度
GLOBAL // 全局维度
}
}
java
package com.seckill.service.aspect;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.annotation.RateLimit;
import com.seckill.service.service.RateLimitService;
import com.seckill.service.utils.IpUtils;
import com.seckill.service.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 限流切面
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final RateLimitService rateLimitService;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
boolean allowed = false;
switch (rateLimit.type()) {
case USER:
Long userId = UserContext.getCurrentUserId();
allowed = rateLimitService.tryAcquire(
userId, 0L, rateLimit.limit(), rateLimit.window());
break;
case IP:
HttpServletRequest request = getRequest();
String ip = IpUtils.getClientIp(request);
allowed = rateLimitService.tryAcquireByIp(
ip, rateLimit.limit(), rateLimit.window());
break;
case GLOBAL:
// 全局限流实现
break;
}
if (!allowed) {
log.warn("请求被限流: type={}", rateLimit.type());
throw new SeckillException(ResultCode.SECKILL_LIMIT);
}
return point.proceed();
}
private HttpServletRequest getRequest() {
ServletRequestAttributes attrs = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
return attrs != null ? attrs.getRequest() : null;
}
}
六、库存扣减方案
6.1 库存扣减方案对比
diff
+------------------+------------------+------------------+------------------+
| 方案 | 优点 | 缺点 | 适用场景 |
+------------------+------------------+------------------+------------------+
| 数据库扣减 | 数据强一致性 | 性能差,并发低 | 低并发场景 |
+------------------+------------------+------------------+------------------+
| Redis预扣减 | 性能高 | 需处理一致性 | 高并发场景 |
+------------------+------------------+------------------+------------------+
| Redis+MQ异步 | 性能最高 | 最终一致性 | 超高并发 |
+------------------+------------------+------------------+------------------+
6.2 Redis库存预热
java
package com.seckill.service.service;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.mapper.SeckillGoodsMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 库存预热服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StockWarmUpService {
private final RedisTemplate<String, Object> redisTemplate;
private final SeckillGoodsMapper seckillGoodsMapper;
private static final String STOCK_KEY_PREFIX = "seckill:stock:";
private static final String GOODS_INFO_PREFIX = "seckill:goods:";
/**
* 系统启动时预热库存
*/
@PostConstruct
public void warmUpOnStartup() {
log.info("系统启动,开始预热秒杀库存...");
warmUpStock();
}
/**
* 定时任务:每5分钟检查并预热即将开始的秒杀活动
*/
@Scheduled(cron = "0 */5 * * * ?")
public void scheduledWarmUp() {
log.info("定时任务:检查秒杀活动库存预热...");
warmUpStock();
}
/**
* 预热库存到Redis
*/
public void warmUpStock() {
// 查询即将开始或正在进行的秒杀活动(未来30分钟内开始的)
LocalDateTime now = LocalDateTime.now();
LocalDateTime future = now.plusMinutes(30);
List<SeckillGoods> goodsList = seckillGoodsMapper.selectActiveGoods(now, future);
for (SeckillGoods goods : goodsList) {
String stockKey = STOCK_KEY_PREFIX + goods.getId();
String infoKey = GOODS_INFO_PREFIX + goods.getId();
// 检查是否已预热
if (Boolean.TRUE.equals(redisTemplate.hasKey(stockKey))) {
log.debug("商品[{}]库存已预热,跳过", goods.getId());
continue;
}
// 预热库存
redisTemplate.opsForValue().set(stockKey, goods.getStockCount());
// 预热商品信息
redisTemplate.opsForValue().set(infoKey, goods);
// 设置过期时间(活动结束后1小时过期)
long expireSeconds = java.time.Duration.between(
now, goods.getEndTime().plusHours(1)).getSeconds();
redisTemplate.expire(stockKey, expireSeconds, TimeUnit.SECONDS);
redisTemplate.expire(infoKey, expireSeconds, TimeUnit.SECONDS);
log.info("商品[{}]库存预热完成,库存数量: {}", goods.getId(), goods.getStockCount());
}
}
/**
* 手动预热指定商品
*/
public void warmUpByGoodsId(Long seckillGoodsId) {
SeckillGoods goods = seckillGoodsMapper.selectById(seckillGoodsId);
if (goods == null) {
log.warn("商品[{}]不存在", seckillGoodsId);
return;
}
String stockKey = STOCK_KEY_PREFIX + goods.getId();
redisTemplate.opsForValue().set(stockKey, goods.getStockCount());
log.info("商品[{}]手动预热完成,库存: {}", seckillGoodsId, goods.getStockCount());
}
}
6.3 Redis + Lua 原子扣减库存
stock_deduct.lua - 库存扣减脚本:
lua
-- 库存扣减Lua脚本
-- KEYS[1]: 库存key
-- KEYS[2]: 已购买用户集合key
-- ARGV[1]: 用户ID
-- ARGV[2]: 扣减数量(默认1)
local stockKey = KEYS[1]
local boughtKey = KEYS[2]
local userId = ARGV[1]
local deductNum = tonumber(ARGV[2]) or 1
-- 检查用户是否已购买
if redis.call('SISMEMBER', boughtKey, userId) == 1 then
return -1 -- 重复购买
end
-- 获取当前库存
local stock = tonumber(redis.call('GET', stockKey) or 0)
if stock < deductNum then
return -2 -- 库存不足
end
-- 扣减库存
redis.call('DECRBY', stockKey, deductNum)
-- 记录用户已购买
redis.call('SADD', boughtKey, userId)
-- 返回剩余库存
return stock - deductNum
6.4 库存服务实现
java
package com.seckill.service.service;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
/**
* 库存服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StockService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedisScript<Long> stockDeductScript;
private static final String STOCK_KEY_PREFIX = "seckill:stock:";
private static final String BOUGHT_KEY_PREFIX = "seckill:bought:";
/**
* 预扣减库存(Redis)
* @return 剩余库存,-1表示重复购买,-2表示库存不足
*/
public long preDeductStock(Long seckillGoodsId, Long userId) {
String stockKey = STOCK_KEY_PREFIX + seckillGoodsId;
String boughtKey = BOUGHT_KEY_PREFIX + seckillGoodsId;
Long result = redisTemplate.execute(
stockDeductScript,
Arrays.asList(stockKey, boughtKey),
userId.toString(), 1
);
if (result == null) {
throw new SeckillException(ResultCode.ERROR);
}
return result;
}
/**
* 回滚库存(订单取消或支付超时)
*/
public void rollbackStock(Long seckillGoodsId, Long userId) {
String stockKey = STOCK_KEY_PREFIX + seckillGoodsId;
String boughtKey = BOUGHT_KEY_PREFIX + seckillGoodsId;
// 库存+1
redisTemplate.opsForValue().increment(stockKey);
// 移除已购买记录
redisTemplate.opsForSet().remove(boughtKey, userId.toString());
log.info("库存回滚成功,seckillGoodsId={}, userId={}", seckillGoodsId, userId);
}
/**
* 获取剩余库存
*/
public Integer getStock(Long seckillGoodsId) {
String stockKey = STOCK_KEY_PREFIX + seckillGoodsId;
Object stock = redisTemplate.opsForValue().get(stockKey);
return stock != null ? Integer.parseInt(stock.toString()) : 0;
}
/**
* 检查用户是否已购买
*/
public boolean hasUserBought(Long seckillGoodsId, Long userId) {
String boughtKey = BOUGHT_KEY_PREFIX + seckillGoodsId;
return Boolean.TRUE.equals(
redisTemplate.opsForSet().isMember(boughtKey, userId.toString()));
}
}
七、异步下单与消息队列
7.1 异步下单流程
lua
+------------+ +------------+ +------------+ +------------+
| 用户请求 | --> | 库存预检 | --> | Redis扣减 | --> | 发送MQ |
+------------+ +------------+ +------------+ +------------+
|
+-------------------------------------------+
|
v
+------------+ +------------+ +------------+ +------------+
| 返回排队 | <-- | 生成排队号 | <-- | 入队成功 | <-- | Producer |
+------------+ +------------+ +------------+ +------------+
+------------+ +------------+ +------------+ +------------+
| Consumer | --> | 创建订单 | --> | 扣减DB库存 | --> | 写入结果 |
+------------+ +------------+ +------------+ +------------+
7.2 RocketMQ消息生产者
java
package com.seckill.service.mq;
import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.SeckillMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
/**
* 秒杀消息生产者
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillMessageProducer {
private final RocketMQTemplate rocketMQTemplate;
private static final String SECKILL_TOPIC = "seckill-order-topic";
/**
* 发送秒杀消息(异步)
*/
public void sendSeckillMessage(SeckillMessage message) {
String jsonMessage = JSON.toJSONString(message);
rocketMQTemplate.asyncSend(SECKILL_TOPIC,
MessageBuilder.withPayload(jsonMessage).build(),
new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("秒杀消息发送成功: msgId={}, userId={}, goodsId={}",
sendResult.getMsgId(), message.getUserId(), message.getSeckillGoodsId());
}
@Override
public void onException(Throwable e) {
log.error("秒杀消息发送失败: userId={}, goodsId={}",
message.getUserId(), message.getSeckillGoodsId(), e);
// 发送失败,回滚Redis库存
// stockService.rollbackStock(message.getSeckillGoodsId(), message.getUserId());
}
});
}
/**
* 发送秒杀消息(同步,用于需要确保消息发送成功的场景)
*/
public SendResult sendSeckillMessageSync(SeckillMessage message) {
String jsonMessage = JSON.toJSONString(message);
return rocketMQTemplate.syncSend(SECKILL_TOPIC,
MessageBuilder.withPayload(jsonMessage).build());
}
}
7.3 消息DTO
java
package com.seckill.service.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 秒杀消息DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 秒杀商品ID
*/
private Long seckillGoodsId;
/**
* 商品ID
*/
private Long goodsId;
/**
* 排队号
*/
private String queueNo;
/**
* 请求时间戳
*/
private Long timestamp;
}
7.4 RocketMQ消息消费者
java
package com.seckill.service.mq;
import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* 秒杀消息消费者
*/
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
topic = "seckill-order-topic",
consumerGroup = "seckill-order-consumer-group",
consumeMode = ConsumeMode.ORDERLY // 顺序消费
)
public class SeckillMessageConsumer implements RocketMQListener<String> {
private final OrderService orderService;
@Override
public void onMessage(String message) {
log.info("收到秒杀消息: {}", message);
try {
SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);
// 创建订单
orderService.createOrder(seckillMessage);
log.info("订单创建成功: userId={}, goodsId={}",
seckillMessage.getUserId(), seckillMessage.getSeckillGoodsId());
} catch (Exception e) {
log.error("处理秒杀消息失败: {}", message, e);
// 这里可以发送到死信队列,或者记录到数据库待后续处理
throw e; // 抛出异常,消息将被重试
}
}
}
7.5 订单服务
java
package com.seckill.service.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.entity.SeckillOrder;
import com.seckill.service.mapper.SeckillGoodsMapper;
import com.seckill.service.mapper.SeckillOrderMapper;
import com.seckill.service.utils.OrderNoGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* 订单服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final SeckillOrderMapper orderMapper;
private final SeckillGoodsMapper goodsMapper;
private final StockService stockService;
private final RedisTemplate<String, Object> redisTemplate;
private static final String ORDER_RESULT_PREFIX = "seckill:result:";
/**
* 创建订单(异步消费时调用)
*/
@Transactional(rollbackFor = Exception.class)
public void createOrder(SeckillMessage message) {
Long userId = message.getUserId();
Long seckillGoodsId = message.getSeckillGoodsId();
// 1. 再次检查是否已下单(幂等性)
SeckillOrder existOrder = orderMapper.selectOne(
new LambdaQueryWrapper<SeckillOrder>()
.eq(SeckillOrder::getUserId, userId)
.eq(SeckillOrder::getSeckillGoodsId, seckillGoodsId)
);
if (existOrder != null) {
log.warn("用户已下单,跳过: userId={}, goodsId={}", userId, seckillGoodsId);
// 写入成功结果
writeResult(userId, seckillGoodsId, existOrder.getOrderNo(), true, "订单已存在");
return;
}
// 2. 查询商品信息
SeckillGoods goods = goodsMapper.selectById(seckillGoodsId);
if (goods == null) {
log.error("商品不存在: seckillGoodsId={}", seckillGoodsId);
writeResult(userId, seckillGoodsId, null, false, "商品不存在");
return;
}
// 3. 数据库扣减库存(乐观锁)
int affected = goodsMapper.deductStock(seckillGoodsId, 1);
if (affected == 0) {
log.warn("数据库库存不足: seckillGoodsId={}", seckillGoodsId);
// 回滚Redis库存
stockService.rollbackStock(seckillGoodsId, userId);
writeResult(userId, seckillGoodsId, null, false, "库存不足");
return;
}
// 4. 创建订单
String orderNo = OrderNoGenerator.generate();
SeckillOrder order = new SeckillOrder();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setGoodsId(goods.getGoodsId());
order.setSeckillGoodsId(seckillGoodsId);
order.setGoodsName(goods.getGoodsName());
order.setGoodsPrice(goods.getSeckillPrice());
order.setOrderStatus(0); // 待支付
orderMapper.insert(order);
// 5. 写入秒杀结果
writeResult(userId, seckillGoodsId, orderNo, true, "秒杀成功");
log.info("订单创建成功: orderNo={}, userId={}, goodsId={}",
orderNo, userId, seckillGoodsId);
}
/**
* 写入秒杀结果到Redis
*/
private void writeResult(Long userId, Long seckillGoodsId, String orderNo,
boolean success, String message) {
String key = ORDER_RESULT_PREFIX + userId + ":" + seckillGoodsId;
SeckillResult result = new SeckillResult(success, orderNo, message);
redisTemplate.opsForValue().set(key, result, 30, TimeUnit.MINUTES);
}
/**
* 查询秒杀结果
*/
public SeckillResult getSeckillResult(Long userId, Long seckillGoodsId) {
String key = ORDER_RESULT_PREFIX + userId + ":" + seckillGoodsId;
return (SeckillResult) redisTemplate.opsForValue().get(key);
}
/**
* 秒杀结果内部类
*/
@lombok.Data
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor
public static class SeckillResult {
private boolean success;
private String orderNo;
private String message;
}
}
7.6 Mapper层
java
package com.seckill.service.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.seckill.service.entity.SeckillGoods;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {
/**
* 乐观锁扣减库存
*/
@Update("UPDATE seckill_goods SET stock_count = stock_count - #{count} " +
"WHERE id = #{id} AND stock_count >= #{count}")
int deductStock(@Param("id") Long id, @Param("count") Integer count);
/**
* 查询活跃的秒杀活动
*/
@Select("SELECT * FROM seckill_goods " +
"WHERE start_time <= #{future} AND end_time >= #{now}")
List<SeckillGoods> selectActiveGoods(@Param("now") LocalDateTime now,
@Param("future") LocalDateTime future);
}
八、秒杀接口实现
8.1 秒杀Controller
java
package com.seckill.service.controller;
import com.seckill.common.result.Result;
import com.seckill.common.result.ResultCode;
import com.seckill.service.annotation.RateLimit;
import com.seckill.service.dto.SeckillRequest;
import com.seckill.service.service.OrderService;
import com.seckill.service.service.SeckillService;
import com.seckill.service.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 秒杀接口
*/
@Slf4j
@RestController
@RequestMapping("/seckill")
@RequiredArgsConstructor
public class SeckillController {
private final SeckillService seckillService;
private final OrderService orderService;
/**
* 执行秒杀
*/
@PostMapping("/do")
@RateLimit(limit = 5, window = 1, type = RateLimit.LimitType.USER)
public Result<String> doSeckill(@Valid @RequestBody SeckillRequest request) {
Long userId = UserContext.getCurrentUserId();
Long seckillGoodsId = request.getSeckillGoodsId();
log.info("秒杀请求: userId={}, goodsId={}", userId, seckillGoodsId);
// 执行秒杀
String queueNo = seckillService.doSeckill(userId, seckillGoodsId);
return Result.success(queueNo);
}
/**
* 查询秒杀结果
*/
@GetMapping("/result/{seckillGoodsId}")
public Result<OrderService.SeckillResult> getSeckillResult(
@PathVariable Long seckillGoodsId) {
Long userId = UserContext.getCurrentUserId();
OrderService.SeckillResult result = orderService.getSeckillResult(userId, seckillGoodsId);
if (result == null) {
// 还在排队
return Result.error(ResultCode.SECKILL_QUEUE);
}
return Result.success(result);
}
/**
* 获取秒杀商品列表
*/
@GetMapping("/goods/list")
public Result<?> getGoodsList() {
return Result.success(seckillService.getSeckillGoodsList());
}
/**
* 获取秒杀商品详情
*/
@GetMapping("/goods/{id}")
public Result<?> getGoodsDetail(@PathVariable Long id) {
return Result.success(seckillService.getSeckillGoodsDetail(id));
}
}
8.2 秒杀核心服务
java
package com.seckill.service.service;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.mapper.SeckillGoodsMapper;
import com.seckill.service.mq.SeckillMessageProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 秒杀核心服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillService {
private final StockService stockService;
private final SeckillMessageProducer messageProducer;
private final SeckillGoodsMapper seckillGoodsMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String GOODS_INFO_PREFIX = "seckill:goods:";
/**
* 执行秒杀
*/
public String doSeckill(Long userId, Long seckillGoodsId) {
// 1. 检查秒杀活动状态
SeckillGoods goods = getSeckillGoods(seckillGoodsId);
checkSeckillTime(goods);
// 2. 检查用户是否已购买(内存标记优化,减少Redis访问)
if (stockService.hasUserBought(seckillGoodsId, userId)) {
throw new SeckillException(ResultCode.SECKILL_REPEAT);
}
// 3. Redis预扣减库存
long stockResult = stockService.preDeductStock(seckillGoodsId, userId);
if (stockResult == -1) {
throw new SeckillException(ResultCode.SECKILL_REPEAT);
}
if (stockResult == -2) {
// 标记商品已售罄(内存标记)
markGoodsSoldOut(seckillGoodsId);
throw new SeckillException(ResultCode.SECKILL_STOCK_EMPTY);
}
// 4. 生成排队号
String queueNo = generateQueueNo(userId, seckillGoodsId);
// 5. 发送MQ消息,异步创建订单
SeckillMessage message = SeckillMessage.builder()
.userId(userId)
.seckillGoodsId(seckillGoodsId)
.goodsId(goods.getGoodsId())
.queueNo(queueNo)
.timestamp(System.currentTimeMillis())
.build();
messageProducer.sendSeckillMessage(message);
log.info("秒杀请求已入队: userId={}, goodsId={}, queueNo={}",
userId, seckillGoodsId, queueNo);
return queueNo;
}
/**
* 获取秒杀商品(优先从缓存获取)
*/
private SeckillGoods getSeckillGoods(Long seckillGoodsId) {
String key = GOODS_INFO_PREFIX + seckillGoodsId;
SeckillGoods goods = (SeckillGoods) redisTemplate.opsForValue().get(key);
if (goods == null) {
goods = seckillGoodsMapper.selectById(seckillGoodsId);
if (goods == null) {
throw new SeckillException(ResultCode.ERROR);
}
}
return goods;
}
/**
* 检查秒杀时间
*/
private void checkSeckillTime(SeckillGoods goods) {
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(goods.getStartTime())) {
throw new SeckillException(ResultCode.SECKILL_NOT_START);
}
if (now.isAfter(goods.getEndTime())) {
throw new SeckillException(ResultCode.SECKILL_END);
}
}
/**
* 标记商品售罄
*/
private void markGoodsSoldOut(Long seckillGoodsId) {
// 可以使用本地缓存或Redis标记
String key = "seckill:soldout:" + seckillGoodsId;
redisTemplate.opsForValue().set(key, true);
}
/**
* 生成排队号
*/
private String generateQueueNo(Long userId, Long seckillGoodsId) {
return String.format("%d-%d-%s",
seckillGoodsId, userId, UUID.randomUUID().toString().substring(0, 8));
}
/**
* 获取秒杀商品列表
*/
public List<SeckillGoods> getSeckillGoodsList() {
LocalDateTime now = LocalDateTime.now();
return seckillGoodsMapper.selectActiveGoods(now, now.plusDays(1));
}
/**
* 获取秒杀商品详情
*/
public SeckillGoods getSeckillGoodsDetail(Long id) {
return getSeckillGoods(id);
}
}
九、分布式锁实现
9.1 Redisson分布式锁
java
package com.seckill.service.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* 分布式锁服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DistributedLockService {
private final RedissonClient redissonClient;
private static final String LOCK_PREFIX = "seckill:lock:";
/**
* 尝试获取锁并执行操作
*/
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit, Supplier<T> supplier) {
RLock lock = redissonClient.getLock(LOCK_PREFIX + lockKey);
try {
boolean acquired = lock.tryLock(waitTime, leaseTime, unit);
if (!acquired) {
log.warn("获取锁失败: lockKey={}", lockKey);
return null;
}
log.debug("获取锁成功: lockKey={}", lockKey);
return supplier.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁被中断: lockKey={}", lockKey, e);
return null;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("释放锁: lockKey={}", lockKey);
}
}
}
/**
* 使用看门狗机制的锁(自动续期)
*/
public <T> T executeWithWatchdog(String lockKey, Supplier<T> supplier) {
RLock lock = redissonClient.getLock(LOCK_PREFIX + lockKey);
try {
// 不指定leaseTime,使用看门狗自动续期
lock.lock();
return supplier.get();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
9.2 Redisson配置
java
package com.seckill.service.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redisson配置
*/
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password:}")
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 单机模式
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(password.isEmpty() ? null : password)
.setDatabase(0)
.setConnectionMinimumIdleSize(10)
.setConnectionPoolSize(64);
// 集群模式(生产环境推荐)
// config.useClusterServers()
// .addNodeAddress("redis://node1:6379", "redis://node2:6379")
// .setPassword(password);
return Redisson.create(config);
}
}
十、高可用与容错
10.1 Sentinel熔断降级配置
java
package com.seckill.service.config;
import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* Sentinel配置
*/
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
@PostConstruct
public void initRules() {
initFlowRules();
initDegradeRules();
}
/**
* 初始化限流规则
*/
private void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 秒杀接口限流:每秒最多处理10000个请求
FlowRule seckillRule = new FlowRule();
seckillRule.setResource("doSeckill");
seckillRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
seckillRule.setCount(10000);
seckillRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
seckillRule.setWarmUpPeriodSec(10); // 预热时间10秒
rules.add(seckillRule);
// 查询接口限流
FlowRule queryRule = new FlowRule();
queryRule.setResource("getSeckillResult");
queryRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
queryRule.setCount(50000);
rules.add(queryRule);
FlowRuleManager.loadRules(rules);
}
/**
* 初始化熔断规则
*/
private void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
// 慢调用比例熔断
DegradeRule slowRule = new DegradeRule();
slowRule.setResource("doSeckill");
slowRule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
slowRule.setCount(100); // 阈值RT(毫秒)
slowRule.setTimeWindow(10); // 熔断时长(秒)
slowRule.setSlowRatioThreshold(0.5); // 慢调用比例阈值
slowRule.setMinRequestAmount(10); // 最小请求数
slowRule.setStatIntervalMs(10000); // 统计时长(毫秒)
rules.add(slowRule);
// 异常比例熔断
DegradeRule exceptionRule = new DegradeRule();
exceptionRule.setResource("doSeckill");
exceptionRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
exceptionRule.setCount(0.5); // 异常比例阈值
exceptionRule.setTimeWindow(30); // 熔断时长(秒)
exceptionRule.setMinRequestAmount(10);
rules.add(exceptionRule);
DegradeRuleManager.loadRules(rules);
}
}
10.2 熔断降级处理
java
package com.seckill.service.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.seckill.common.result.Result;
import com.seckill.common.result.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 带熔断降级的秒杀服务
*/
@Slf4j
@Service
public class SeckillServiceWithSentinel {
@SentinelResource(
value = "doSeckill",
blockHandler = "doSeckillBlockHandler",
fallback = "doSeckillFallback"
)
public Result<String> doSeckillWithSentinel(Long userId, Long seckillGoodsId) {
// 秒杀逻辑
return Result.success("排队中");
}
/**
* 限流/熔断处理
*/
public Result<String> doSeckillBlockHandler(Long userId, Long seckillGoodsId,
BlockException ex) {
log.warn("秒杀接口被限流/熔断: userId={}, goodsId={}", userId, seckillGoodsId);
return Result.error(ResultCode.SECKILL_LIMIT);
}
/**
* 降级处理
*/
public Result<String> doSeckillFallback(Long userId, Long seckillGoodsId,
Throwable ex) {
log.error("秒杀接口异常降级: userId={}, goodsId={}", userId, seckillGoodsId, ex);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
十一、订单超时处理
11.1 延迟队列方案
lua
+-------------------+
| 创建订单 |
+--------+----------+
|
+--------v----------+
| 发送延迟消息 |
| (30分钟后触发) |
+--------+----------+
|
| 30分钟后
|
+--------v----------+
| 消费延迟消息 |
+--------+----------+
|
+--------------+--------------+
| |
+---------v---------+ +---------v---------+
| 订单已支付 | | 订单未支付 |
| 忽略消息 | | 取消订单 |
+-------------------+ | 回滚库存 |
+-------------------+
11.2 RocketMQ延迟消息实现
java
package com.seckill.service.mq;
import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.OrderTimeoutMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
/**
* 订单超时消息生产者
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderTimeoutProducer {
private final RocketMQTemplate rocketMQTemplate;
private static final String TOPIC = "order-timeout-topic";
// RocketMQ延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
private static final int DELAY_LEVEL_30_MIN = 16; // 30分钟
/**
* 发送订单超时检查消息
*/
public void sendOrderTimeoutMessage(String orderNo, Long userId, Long seckillGoodsId) {
OrderTimeoutMessage message = new OrderTimeoutMessage();
message.setOrderNo(orderNo);
message.setUserId(userId);
message.setSeckillGoodsId(seckillGoodsId);
message.setCreateTime(System.currentTimeMillis());
String jsonMessage = JSON.toJSONString(message);
Message<String> msg = MessageBuilder
.withPayload(jsonMessage)
.build();
// 发送延迟消息
rocketMQTemplate.syncSend(TOPIC, msg, 3000, DELAY_LEVEL_30_MIN);
log.info("订单超时检查消息已发送: orderNo={}", orderNo);
}
}
java
package com.seckill.service.mq;
import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.OrderTimeoutMessage;
import com.seckill.service.service.OrderCancelService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* 订单超时消息消费者
*/
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
topic = "order-timeout-topic",
consumerGroup = "order-timeout-consumer-group"
)
public class OrderTimeoutConsumer implements RocketMQListener<String> {
private final OrderCancelService orderCancelService;
@Override
public void onMessage(String message) {
log.info("收到订单超时消息: {}", message);
try {
OrderTimeoutMessage timeoutMessage = JSON.parseObject(message,
OrderTimeoutMessage.class);
// 处理订单超时
orderCancelService.handleOrderTimeout(timeoutMessage.getOrderNo());
} catch (Exception e) {
log.error("处理订单超时消息失败: {}", message, e);
}
}
}
11.3 订单取消服务
java
package com.seckill.service.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.seckill.service.entity.SeckillOrder;
import com.seckill.service.mapper.SeckillGoodsMapper;
import com.seckill.service.mapper.SeckillOrderMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 订单取消服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderCancelService {
private final SeckillOrderMapper orderMapper;
private final SeckillGoodsMapper goodsMapper;
private final StockService stockService;
/**
* 处理订单超时
*/
@Transactional(rollbackFor = Exception.class)
public void handleOrderTimeout(String orderNo) {
// 1. 查询订单
SeckillOrder order = orderMapper.selectOne(
new LambdaQueryWrapper<SeckillOrder>()
.eq(SeckillOrder::getOrderNo, orderNo)
);
if (order == null) {
log.warn("订单不存在: orderNo={}", orderNo);
return;
}
// 2. 检查订单状态
if (order.getOrderStatus() != 0) {
// 非待支付状态,不处理
log.info("订单状态非待支付,跳过: orderNo={}, status={}",
orderNo, order.getOrderStatus());
return;
}
// 3. 更新订单状态为已取消
int affected = orderMapper.update(null,
new LambdaUpdateWrapper<SeckillOrder>()
.eq(SeckillOrder::getOrderNo, orderNo)
.eq(SeckillOrder::getOrderStatus, 0) // 乐观锁
.set(SeckillOrder::getOrderStatus, 2) // 已取消
);
if (affected == 0) {
log.info("订单状态已变更,跳过: orderNo={}", orderNo);
return;
}
// 4. 恢复数据库库存
goodsMapper.deductStock(order.getSeckillGoodsId(), -1); // 负数表示增加
// 5. 恢复Redis库存
stockService.rollbackStock(order.getSeckillGoodsId(), order.getUserId());
log.info("订单超时取消成功: orderNo={}", orderNo);
}
}
十二、安全防护
12.1 接口防重放
java
package com.seckill.service.interceptor;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* 防重放攻击拦截器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ReplayAttackInterceptor implements HandlerInterceptor {
private final RedisTemplate<String, Object> redisTemplate;
private static final String NONCE_PREFIX = "seckill:nonce:";
private static final long NONCE_EXPIRE_SECONDS = 300; // 5分钟
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
// 获取请求头中的签名信息
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
String signature = request.getHeader("X-Signature");
// 1. 验证时间戳(防止重放攻击)
if (!validateTimestamp(timestamp)) {
throw new SeckillException(ResultCode.ERROR);
}
// 2. 验证nonce(防止重复请求)
if (!validateNonce(nonce)) {
throw new SeckillException(ResultCode.ERROR);
}
// 3. 验证签名
if (!validateSignature(request, timestamp, nonce, signature)) {
throw new SeckillException(ResultCode.ERROR);
}
return true;
}
private boolean validateTimestamp(String timestamp) {
if (timestamp == null) {
return false;
}
try {
long ts = Long.parseLong(timestamp);
long now = System.currentTimeMillis();
// 请求时间与服务器时间差不超过5分钟
return Math.abs(now - ts) < NONCE_EXPIRE_SECONDS * 1000;
} catch (NumberFormatException e) {
return false;
}
}
private boolean validateNonce(String nonce) {
if (nonce == null || nonce.isEmpty()) {
return false;
}
String key = NONCE_PREFIX + nonce;
Boolean success = redisTemplate.opsForValue().setIfAbsent(
key, "1", NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
private boolean validateSignature(HttpServletRequest request, String timestamp,
String nonce, String signature) {
// 获取密钥(可以从用户Token中获取)
String secretKey = "user_secret_key";
// 构建签名字符串
String signStr = String.format("%s&%s&%s&%s",
request.getMethod(),
request.getRequestURI(),
timestamp,
nonce
);
// 计算签名
String expectedSignature = DigestUtils.md5DigestAsHex(
(signStr + "&" + secretKey).getBytes());
return expectedSignature.equals(signature);
}
}
12.2 验证码防护
java
package com.seckill.service.service;
import com.google.code.kaptcha.Producer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 验证码服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CaptchaService {
private final Producer captchaProducer;
private final RedisTemplate<String, Object> redisTemplate;
private static final String CAPTCHA_PREFIX = "seckill:captcha:";
private static final long CAPTCHA_EXPIRE_SECONDS = 300; // 5分钟
/**
* 生成验证码
*/
public CaptchaResult generateCaptcha() {
// 生成验证码文本
String captchaText = captchaProducer.createText();
// 生成验证码图片
BufferedImage image = captchaProducer.createImage(captchaText);
// 转换为Base64
String base64Image = imageToBase64(image);
// 生成Token
String captchaToken = UUID.randomUUID().toString();
// 存储到Redis
String key = CAPTCHA_PREFIX + captchaToken;
redisTemplate.opsForValue().set(key, captchaText,
CAPTCHA_EXPIRE_SECONDS, TimeUnit.SECONDS);
return new CaptchaResult(captchaToken, "data:image/png;base64," + base64Image);
}
/**
* 验证验证码
*/
public boolean verifyCaptcha(String captchaToken, String captchaCode) {
if (captchaToken == null || captchaCode == null) {
return false;
}
String key = CAPTCHA_PREFIX + captchaToken;
String storedCode = (String) redisTemplate.opsForValue().get(key);
// 验证后删除
redisTemplate.delete(key);
return captchaCode.equalsIgnoreCase(storedCode);
}
private String imageToBase64(BufferedImage image) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
log.error("图片转Base64失败", e);
return "";
}
}
@lombok.Data
@lombok.AllArgsConstructor
public static class CaptchaResult {
private String token;
private String image;
}
}
十三、性能优化
13.1 本地缓存优化
java
package com.seckill.service.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.seckill.service.entity.SeckillGoods;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 本地缓存(Caffeine)
*/
@Component
public class LocalCache {
/**
* 商品信息缓存
*/
private final Cache<Long, SeckillGoods> goodsCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
/**
* 商品售罄标记缓存
*/
private final Cache<Long, Boolean> soldOutCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.SECONDS)
.build();
/**
* 用户已购买标记缓存
*/
private final Cache<String, Boolean> userBoughtCache = Caffeine.newBuilder()
.maximumSize(100000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
// 商品信息缓存操作
public void putGoods(Long goodsId, SeckillGoods goods) {
goodsCache.put(goodsId, goods);
}
public SeckillGoods getGoods(Long goodsId) {
return goodsCache.getIfPresent(goodsId);
}
// 售罄标记操作
public void markSoldOut(Long goodsId) {
soldOutCache.put(goodsId, true);
}
public boolean isSoldOut(Long goodsId) {
return Boolean.TRUE.equals(soldOutCache.getIfPresent(goodsId));
}
// 用户购买标记操作
public void markUserBought(Long userId, Long goodsId) {
userBoughtCache.put(userId + ":" + goodsId, true);
}
public boolean hasUserBought(Long userId, Long goodsId) {
return Boolean.TRUE.equals(userBoughtCache.getIfPresent(userId + ":" + goodsId));
}
}
13.2 优化后的秒杀服务
java
package com.seckill.service.service;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.cache.LocalCache;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.mq.SeckillMessageProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 优化后的秒杀服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OptimizedSeckillService {
private final LocalCache localCache;
private final StockService stockService;
private final SeckillMessageProducer messageProducer;
/**
* 优化后的秒杀流程
*/
public String doSeckill(Long userId, Long seckillGoodsId) {
// 第1层:本地缓存检查售罄标记(内存级别,纳秒级)
if (localCache.isSoldOut(seckillGoodsId)) {
throw new SeckillException(ResultCode.SECKILL_STOCK_EMPTY);
}
// 第2层:本地缓存检查用户是否已购买
if (localCache.hasUserBought(userId, seckillGoodsId)) {
throw new SeckillException(ResultCode.SECKILL_REPEAT);
}
// 第3层:获取商品信息(本地缓存 + Redis二级缓存)
SeckillGoods goods = getGoodsWithCache(seckillGoodsId);
// 第4层:检查秒杀时间
checkSeckillTime(goods);
// 第5层:Redis预扣减库存(原子操作)
long stockResult = stockService.preDeductStock(seckillGoodsId, userId);
if (stockResult == -1) {
localCache.markUserBought(userId, seckillGoodsId);
throw new SeckillException(ResultCode.SECKILL_REPEAT);
}
if (stockResult == -2) {
localCache.markSoldOut(seckillGoodsId);
throw new SeckillException(ResultCode.SECKILL_STOCK_EMPTY);
}
// 标记用户已购买
localCache.markUserBought(userId, seckillGoodsId);
// 如果库存即将售罄,标记售罄
if (stockResult <= 0) {
localCache.markSoldOut(seckillGoodsId);
}
// 第6层:发送MQ消息,异步创建订单
String queueNo = generateQueueNo(userId, seckillGoodsId);
SeckillMessage message = SeckillMessage.builder()
.userId(userId)
.seckillGoodsId(seckillGoodsId)
.goodsId(goods.getGoodsId())
.queueNo(queueNo)
.timestamp(System.currentTimeMillis())
.build();
messageProducer.sendSeckillMessage(message);
return queueNo;
}
private SeckillGoods getGoodsWithCache(Long seckillGoodsId) {
// 先查本地缓存
SeckillGoods goods = localCache.getGoods(seckillGoodsId);
if (goods != null) {
return goods;
}
// 再查Redis/DB
goods = stockService.getGoodsFromRedis(seckillGoodsId);
if (goods != null) {
localCache.putGoods(seckillGoodsId, goods);
}
return goods;
}
private void checkSeckillTime(SeckillGoods goods) {
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(goods.getStartTime())) {
throw new SeckillException(ResultCode.SECKILL_NOT_START);
}
if (now.isAfter(goods.getEndTime())) {
throw new SeckillException(ResultCode.SECKILL_END);
}
}
private String generateQueueNo(Long userId, Long seckillGoodsId) {
return String.format("%d-%d-%s",
seckillGoodsId, userId, UUID.randomUUID().toString().substring(0, 8));
}
}
13.3 请求过滤优化
lua
用户请求(100万QPS)
|
+------------+------------+
| 第1层过滤 |
| 本地缓存售罄检查 |
| 过滤掉90%请求 |
+------------+------------+
|
10万QPS
|
+------------+------------+
| 第2层过滤 |
| 用户重复购买检查 |
| 过滤掉50%请求 |
+------------+------------+
|
5万QPS
|
+------------+------------+
| 第3层过滤 |
| Redis库存预扣减 |
| 按实际库存过滤 |
+------------+------------+
|
1000 QPS
|
+------------+------------+
| 第4层 |
| 消息队列异步处理 |
| 最终创建订单 |
+------------+------------+
十四、监控与告警
14.1 Prometheus监控指标
java
package com.seckill.service.monitor;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* 秒杀监控指标
*/
@Component
@RequiredArgsConstructor
public class SeckillMetrics {
private final MeterRegistry meterRegistry;
private Counter seckillRequestCounter;
private Counter seckillSuccessCounter;
private Counter seckillFailCounter;
private Counter stockEmptyCounter;
private Counter repeatBuyCounter;
private Timer seckillTimer;
@PostConstruct
public void init() {
// 秒杀请求总数
seckillRequestCounter = Counter.builder("seckill_request_total")
.description("秒杀请求总数")
.register(meterRegistry);
// 秒杀成功数
seckillSuccessCounter = Counter.builder("seckill_success_total")
.description("秒杀成功总数")
.register(meterRegistry);
// 秒杀失败数
seckillFailCounter = Counter.builder("seckill_fail_total")
.description("秒杀失败总数")
.register(meterRegistry);
// 库存不足次数
stockEmptyCounter = Counter.builder("seckill_stock_empty_total")
.description("库存不足次数")
.register(meterRegistry);
// 重复购买次数
repeatBuyCounter = Counter.builder("seckill_repeat_buy_total")
.description("重复购买次数")
.register(meterRegistry);
// 秒杀耗时
seckillTimer = Timer.builder("seckill_duration")
.description("秒杀接口耗时")
.register(meterRegistry);
}
public void recordRequest() {
seckillRequestCounter.increment();
}
public void recordSuccess() {
seckillSuccessCounter.increment();
}
public void recordFail() {
seckillFailCounter.increment();
}
public void recordStockEmpty() {
stockEmptyCounter.increment();
}
public void recordRepeatBuy() {
repeatBuyCounter.increment();
}
public void recordDuration(long durationMs) {
seckillTimer.record(durationMs, TimeUnit.MILLISECONDS);
}
}
14.2 日志规范
java
package com.seckill.service.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 秒杀日志切面
*/
@Slf4j
@Aspect
@Component
public class SeckillLogAspect {
@Around("execution(* com.seckill.service.service.SeckillService.doSeckill(..))")
public Object logSeckill(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
Long userId = (Long) args[0];
Long goodsId = (Long) args[1];
long startTime = System.currentTimeMillis();
String traceId = generateTraceId();
log.info("[秒杀] traceId={}, userId={}, goodsId={}, action=START",
traceId, userId, goodsId);
try {
Object result = point.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("[秒杀] traceId={}, userId={}, goodsId={}, action=SUCCESS, " +
"duration={}ms, result={}",
traceId, userId, goodsId, duration, result);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.warn("[秒杀] traceId={}, userId={}, goodsId={}, action=FAIL, " +
"duration={}ms, error={}",
traceId, userId, goodsId, duration, e.getMessage());
throw e;
}
}
private String generateTraceId() {
return String.valueOf(System.nanoTime());
}
}
十五、配置文件
15.1 application.yml
yaml
server:
port: 8080
tomcat:
threads:
max: 500
min-spare: 50
accept-count: 1000
spring:
application:
name: seckill-service
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
minimum-idle: 10
maximum-pool-size: 50
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
# Redis配置
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 100
max-idle: 50
min-idle: 10
max-wait: 3000ms
# RocketMQ配置
rocketmq:
name-server: localhost:9876
producer:
group: seckill-producer-group
send-message-timeout: 3000
retry-times-when-send-failed: 2
retry-times-when-send-async-failed: 2
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志配置
logging:
level:
com.seckill: debug
org.springframework: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
# 秒杀配置
seckill:
# 库存预热提前时间(分钟)
warmup-advance-minutes: 30
# 订单超时时间(分钟)
order-timeout-minutes: 30
# 用户限流阈值
user-rate-limit: 5
# IP限流阈值
ip-rate-limit: 10
十六、总结
16.1 核心技术点回顾
sql
+------------------+------------------+----------------------------------+
| 层级 | 技术 | 作用 |
+------------------+------------------+----------------------------------+
| 接入层 | Nginx | 负载均衡、静态资源、限流 |
+------------------+------------------+----------------------------------+
| 网关层 | Gateway | 统一入口、认证、路由 |
+------------------+------------------+----------------------------------+
| 限流层 | Sentinel/Lua | 多维度限流、熔断降级 |
+------------------+------------------+----------------------------------+
| 缓存层 | Redis/Local | 库存预热、分布式锁 |
+------------------+------------------+----------------------------------+
| 消息层 | RocketMQ | 异步削峰、延迟队列 |
+------------------+------------------+----------------------------------+
| 服务层 | Spring Boot | 业务逻辑处理 |
+------------------+------------------+----------------------------------+
| 数据层 | MySQL | 数据持久化、乐观锁 |
+------------------+------------------+----------------------------------+
16.2 关键设计原则
- 流量漏斗:层层过滤,减少到达数据库的请求
- 异步解耦:消息队列削峰填谷,提高系统吞吐量
- 缓存为王:多级缓存策略,减少数据库压力
- 原子操作:Lua脚本保证库存扣减的原子性
- 降级兜底:熔断降级保护系统稳定性
- 幂等设计:确保重复请求不会产生副作用
16.3 生产环境建议
- 容量规划:根据预估QPS规划服务器数量
- 压力测试:上线前进行全链路压测
- 灰度发布:逐步放开流量,观察系统表现
- 监控告警:完善的监控体系,及时发现问题
- 应急预案:准备降级方案,应对突发情况