
中秋佳节临近,月饼作为传统节庆食品,成为各大电商平台和线下商家的营销焦点。而 "月饼秒杀" 活动,以其限时、限量、低价的特点,成为吸引用户、提升销量的重要手段。
然而,看似简单的 "秒杀" 背后,隐藏着巨大的技术挑战:当数万甚至数百万用户同时抢购限量的月饼礼盒时,如何保证系统不崩溃?如何防止超卖?如何确保公平性?如何处理突发流量?
本文将以中秋月饼秒杀为背景,从需求分析到架构设计,从核心算法到代码实现,全方位解析一个高并发秒杀系统的构建过程。无论你是电商平台的技术负责人,还是希望了解高并发系统设计的开发者,本文都将为你提供实用的技术参考和实践指导。
在开始设计之前,我们首先需要明确月饼秒杀系统的核心需求。一个完善的月饼秒杀系统应该具备以下功能和特性:
月饼秒杀活动具有以下独特的业务特点:
基于上述需求分析,我们设计如下月饼秒杀系统架构:

月饼秒杀系统的核心流程如下:

基于 MySQL 8.0 设计如下数据库表结构:
CREATE TABLE `moon_cake_seckill_activity` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动ID',
`activity_name` varchar(128) NOT NULL COMMENT '活动名称',
`activity_desc` varchar(512) DEFAULT NULL COMMENT '活动描述',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-草稿,1-已发布,2-已开始,3-已结束',
`total_person` int NOT NULL DEFAULT '0' COMMENT '参与总人数限制,0表示无限制',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_status_time` (`status`,`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月饼秒杀活动表';
CREATE TABLE `moon_cake_seckill_goods` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
`activity_id` bigint NOT NULL COMMENT '活动ID',
`goods_id` bigint NOT NULL COMMENT '商品ID',
`goods_name` varchar(128) NOT NULL COMMENT '商品名称',
`goods_image` varchar(256) DEFAULT NULL COMMENT '商品图片URL',
`seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格',
`original_price` decimal(10,2) NOT NULL COMMENT '原价',
`total_stock` int NOT NULL COMMENT '总库存',
`available_stock` int NOT NULL COMMENT '可用库存',
`per_user_limit` int NOT NULL DEFAULT '1' COMMENT '每人限购数量',
`sort` int NOT NULL DEFAULT '0' COMMENT '排序',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_activity_goods` (`activity_id`,`goods_id`),
KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月饼秒杀商品表';
CREATE TABLE `moon_cake_seckill_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`activity_id` bigint NOT NULL COMMENT '活动ID',
`goods_id` bigint NOT NULL COMMENT '商品ID',
`goods_name` varchar(128) NOT NULL COMMENT '商品名称',
`seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格',
`quantity` int NOT NULL DEFAULT '1' COMMENT '购买数量',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`status` tinyint NOT NULL COMMENT '订单状态:0-待支付,1-已支付,2-已取消,3-已超时',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`cancel_time` datetime DEFAULT NULL COMMENT '取消时间',
`expire_time` datetime NOT NULL COMMENT '过期时间',
`address_id` bigint DEFAULT NULL COMMENT '收货地址ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_activity` (`user_id`,`activity_id`),
KEY `idx_status_expire` (`status`,`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月饼秒杀订单表';
CREATE TABLE `moon_cake_seckill_user_stat` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`activity_id` bigint NOT NULL COMMENT '活动ID',
`goods_id` bigint NOT NULL COMMENT '商品ID',
`success_count` int NOT NULL DEFAULT '0' COMMENT '成功抢购次数',
`total_count` int NOT NULL DEFAULT '0' COMMENT '总参与次数',
`last_seckill_time` datetime DEFAULT NULL COMMENT '最后一次参与时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_activity_goods` (`user_id`,`activity_id`,`goods_id`),
KEY `idx_user_activity` (`user_id`,`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户秒杀记录统计表';
基于需求分析和架构设计,我们选择以下技术栈:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.mooncake</groupId>
<artifactId>seckill-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill-system</name>
<description>中秋月饼秒杀系统</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<springdoc.version>2.1.0</springdoc.version>
<lombok.version>1.18.30</lombok.version>
<fastjson2.version>2.0.41</fastjson2.version>
<guava.version>32.1.3-jre</guava.version>
<redisson.version>3.23.3</redisson.version>
<resilience4j.version>2.1.0</resilience4j.version>
<quartz.version>2.3.2</quartz.version>
</properties>
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- 消息队列 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 限流 -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- 任务调度 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring:
profiles:
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/moon_cake_seckill?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 30
minimum-idle: 10
idle-timeout: 300000
connection-timeout: 20000
redis:
cluster:
nodes:
- localhost:6379
- localhost:6380
- localhost:6381
password:
timeout: 3000ms
lettuce:
pool:
max-active: 50
max-idle: 20
min-idle: 10
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual
concurrency: 10
max-concurrency: 20
publisher-confirm-type: correlated
publisher-returns: true
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.mooncake.seckill.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
packages-to-scan: com.mooncake.seckill.controller
redisson:
cluster-servers-config:
scan-interval: 2000
node-addresses:
- redis://localhost:6379
- redis://localhost:6380
- redis://localhost:6381
password:
connection-pool-size: 32
connection-minimum-idle-size: 8
resilience4j:
ratelimiter:
instances:
seckillApi:
limit-refresh-period: 1s
limit-for-period: 1000
timeout-duration: 0
circuitbreaker:
instances:
seckillService:
sliding-window-size: 100
failure-rate-threshold: 50
wait-duration-in-open-state: 10000
permitted-number-of-calls-in-half-open-state: 10
logging:
level:
root: info
com.mooncake.seckill: debug
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file:
name: logs/moon-cake-seckill.log
server:
port: 8080
servlet:
context-path: /seckill
tomcat:
threads:
max: 300
connection-timeout: 30000
# 秒杀相关配置
seckill:
stock:
cache-prefix: "seckill:stock:"
local-cache-expire: 60000
user:
limit-prefix: "seckill:user:limit:"
order:
pay-timeout: 15 # 支付超时时间,单位分钟
redis:
lock-prefix: "seckill:lock:"
lock-expire: 30000 # 锁过期时间,单位毫秒
activity:
cache-prefix: "seckill:activity:"
首先,我们定义核心实体类,对应数据库表结构:
package com.mooncake.seckill.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 月饼秒杀活动实体类
*
* @author 果酱
*/
@Data
@TableName("moon_cake_seckill_activity")
public class MoonCakeSeckillActivity {
/**
* 活动ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 活动名称
*/
private String activityName;
/**
* 活动描述
*/
private String activityDesc;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 状态:0-草稿,1-已发布,2-已开始,3-已结束
*/
private Integer status;
/**
* 参与总人数限制,0表示无限制
*/
private Integer totalPerson;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.mooncake.seckill.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 月饼秒杀商品实体类
*
* @author 果酱
*/
@Data
@TableName("moon_cake_seckill_goods")
public class MoonCakeSeckillGoods {
/**
* 秒杀商品ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 活动ID
*/
private Long activityId;
/**
* 商品ID
*/
private Long goodsId;
/**
* 商品名称
*/
private String goodsName;
/**
* 商品图片URL
*/
private String goodsImage;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 原价
*/
private BigDecimal originalPrice;
/**
* 总库存
*/
private Integer totalStock;
/**
* 可用库存
*/
private Integer availableStock;
/**
* 每人限购数量
*/
private Integer perUserLimit;
/**
* 排序
*/
private Integer sort;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.mooncake.seckill.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 月饼秒杀订单实体类
*
* @author 果酱
*/
@Data
@TableName("moon_cake_seckill_order")
public class MoonCakeSeckillOrder {
/**
* 订单ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 订单编号
*/
private String orderNo;
/**
* 用户ID
*/
private Long userId;
/**
* 活动ID
*/
private Long activityId;
/**
* 商品ID
*/
private Long goodsId;
/**
* 商品名称
*/
private String goodsName;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 购买数量
*/
private Integer quantity;
/**
* 订单总金额
*/
private BigDecimal totalAmount;
/**
* 订单状态:0-待支付,1-已支付,2-已取消,3-已超时
*/
private Integer status;
/**
* 支付时间
*/
private LocalDateTime payTime;
/**
* 取消时间
*/
private LocalDateTime cancelTime;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 收货地址ID
*/
private Long addressId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.mooncake.seckill.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户秒杀记录统计实体类
*
* @author 果酱
*/
@Data
@TableName("moon_cake_seckill_user_stat")
public class MoonCakeSeckillUserStat {
/**
* ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 活动ID
*/
private Long activityId;
/**
* 商品ID
*/
private Long goodsId;
/**
* 成功抢购次数
*/
private Integer successCount;
/**
* 总参与次数
*/
private Integer totalCount;
/**
* 最后一次参与时间
*/
private LocalDateTime lastSeckillTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
使用 MyBatis-Plus 的 BaseMapper 简化数据访问层代码:
package com.mooncake.seckill.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mooncake.seckill.entity.MoonCakeSeckillActivity;
import org.apache.ibatis.annotations.Mapper;
/**
* 月饼秒杀活动Mapper
*
* @author 果酱
*/
@Mapper
public interface MoonCakeSeckillActivityMapper extends BaseMapper<MoonCakeSeckillActivity> {
}
其他实体的 Mapper 类实现类似,这里省略。
package com.mooncake.seckill.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mooncake.seckill.entity.MoonCakeSeckillGoods;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 月饼秒杀商品Mapper
*
* @author 果酱
*/
@Mapper
public interface MoonCakeSeckillGoodsMapper extends BaseMapper<MoonCakeSeckillGoods> {
/**
* 扣减库存
*
* @param id 商品ID
* @param quantity 扣减数量
* @return 影响行数
*/
int deductStock(@Param("id") Long id, @Param("quantity") Integer quantity);
/**
* 恢复库存
*
* @param id 商品ID
* @param quantity 恢复数量
* @return 影响行数
*/
int recoverStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}
服务层是秒杀系统的核心,包含了业务逻辑的实现。我们采用分层设计,将服务分为接口和实现:
package com.mooncake.seckill.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mooncake.seckill.entity.MoonCakeSeckillOrder;
import com.mooncake.seckill.vo.SeckillResultVO;
/**
* 秒杀服务接口
*
* @author 果酱
*/
public interface SeckillService extends IService<MoonCakeSeckillOrder> {
/**
* 执行秒杀操作
*
* @param userId 用户ID
* @param activityId 活动ID
* @param goodsId 商品ID
* @param quantity 购买数量
* @return 秒杀结果
*/
SeckillResultVO doSeckill(Long userId, Long activityId, Long goodsId, Integer quantity);
/**
* 预热秒杀商品库存到缓存
*
* @param activityId 活动ID
*/
void preloadStockToCache(Long activityId);
/**
* 查询秒杀商品的实时库存
*
* @param activityId 活动ID
* @param goodsId 商品ID
* @return 库存数量
*/
Integer getSeckillStock(Long activityId, Long goodsId);
/**
* 处理超时未支付的订单
*
* @param orderNo 订单编号
*/
void handleTimeoutOrder(String orderNo);
}
package com.mooncake.seckill.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Maps;
import com.mooncake.seckill.config.SeckillProperties;
import com.mooncake.seckill.entity.MoonCakeSeckillActivity;
import com.mooncake.seckill.entity.MoonCakeSeckillGoods;
import com.mooncake.seckill.entity.MoonCakeSeckillOrder;
import com.mooncake.seckill.entity.MoonCakeSeckillUserStat;
import com.mooncake.seckill.enums.ActivityStatusEnum;
import com.mooncake.seckill.enums.ErrorCodeEnum;
import com.mooncake.seckill.enums.OrderStatusEnum;
import com.mooncake.seckill.exception.BusinessException;
import com.mooncake.seckill.mapper.MoonCakeSeckillOrderMapper;
import com.mooncake.seckill.mq.producer.SeckillMessageProducer;
import com.mooncake.seckill.service.*;
import com.mooncake.seckill.vo.SeckillResultVO;
import com.mooncake.seckill.vo.UserSeckillVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 秒杀服务实现类
*
* @author 果酱
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillServiceImpl extends ServiceImpl<MoonCakeSeckillOrderMapper, MoonCakeSeckillOrder> implements SeckillService {
private final MoonCakeSeckillActivityService activityService;
private final MoonCakeSeckillGoodsService seckillGoodsService;
private final MoonCakeSeckillUserStatService userStatService;
private final SeckillMessageProducer messageProducer;
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
private final CacheManager cacheManager;
private final SeckillProperties seckillProperties;
/**
* 执行秒杀操作
*
* @param userId 用户ID
* @param activityId 活动ID
* @param goodsId 商品ID
* @param quantity 购买数量
* @return 秒杀结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public SeckillResultVO doSeckill(Long userId, Long activityId, Long goodsId, Integer quantity) {
// 参数校验
validateSeckillParams(userId, activityId, goodsId, quantity);
// 1. 检查活动状态
MoonCakeSeckillActivity activity = checkActivityStatus(activityId);
// 2. 检查商品信息和库存
MoonCakeSeckillGoods seckillGoods = checkSeckillGoods(activityId, goodsId, quantity);
// 3. 检查用户参与条件
checkUserConditions(userId, activityId, goodsId, quantity, activity, seckillGoods);
// 4. 检查用户频率限制
checkUserFrequency(userId, activityId, goodsId);
// 5. 检查缓存库存并预扣减
boolean cacheDeductSuccess = deductStockInCache(activityId, goodsId, quantity);
if (!cacheDeductSuccess) {
log.warn("缓存库存不足或扣减失败,userId: {}, activityId: {}, goodsId: {}", userId, activityId, goodsId);
throw new BusinessException(ErrorCodeEnum.STOCK_INSUFFICIENT);
}
// 6. 获取分布式锁,防止超卖
String lockKey = seckillProperties.getRedis().getLockPrefix() + goodsId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待1秒,30秒后自动释放
boolean locked = lock.tryLock(1, seckillProperties.getRedis().getLockExpire(), TimeUnit.MILLISECONDS);
if (!locked) {
log.warn("用户获取秒杀锁失败,可能系统繁忙,请重试,userId: {}, goodsId: {}", userId, goodsId);
// 回滚缓存库存
recoverStockInCache(activityId, goodsId, quantity);
throw new BusinessException(ErrorCodeEnum.SYSTEM_BUSY_RETRY);
}
// 7. 再次检查数据库库存并扣减
int affectRows = seckillGoodsService.deductStock(seckillGoods.getId(), quantity);
if (affectRows <= 0) {
log.warn("数据库库存不足,扣减失败,userId: {}, goodsId: {}", userId, goodsId);
// 回滚缓存库存
recoverStockInCache(activityId, goodsId, quantity);
throw new BusinessException(ErrorCodeEnum.STOCK_INSUFFICIENT);
}
// 8. 创建秒杀订单
MoonCakeSeckillOrder order = createSeckillOrder(userId, activityId, seckillGoods, quantity);
// 9. 更新用户统计信息
updateUserStat(userId, activityId, goodsId, true);
// 10. 发送订单创建消息到MQ,异步处理后续流程
messageProducer.sendOrderCreatedMessage(buildUserSeckillVO(order, seckillGoods));
// 11. 返回秒杀成功结果
SeckillResultVO resultVO = new SeckillResultVO();
resultVO.setSuccess(true);
resultVO.setOrderNo(order.getOrderNo());
resultVO.setGoodsId(goodsId);
resultVO.setActivityId(activityId);
resultVO.setMessage("恭喜,秒杀成功!");
return resultVO;
} catch (InterruptedException e) {
log.error("获取秒杀锁中断,userId: {}, goodsId: {}", userId, goodsId, e);
// 回滚缓存库存
recoverStockInCache(activityId, goodsId, quantity);
Thread.currentThread().interrupt();
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR);
} catch (BusinessException e) {
log.error("秒杀业务异常,userId: {}, goodsId: {}, message: {}", userId, goodsId, e.getMessage());
// 回滚缓存库存
recoverStockInCache(activityId, goodsId, quantity);
throw e;
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 构建用户秒杀信息VO
*/
private UserSeckillVO buildUserSeckillVO(MoonCakeSeckillOrder order, MoonCakeSeckillGoods goods) {
UserSeckillVO vo = new UserSeckillVO();
vo.setOrderNo(order.getOrderNo());
vo.setUserId(order.getUserId());
vo.setActivityId(order.getActivityId());
vo.setGoodsId(order.getGoodsId());
vo.setGoodsName(order.getGoodsName());
vo.setQuantity(order.getQuantity());
vo.setTotalAmount(order.getTotalAmount());
vo.setExpireTime(order.getExpireTime());
vo.setSeckillPrice(goods.getSeckillPrice());
return vo;
}
/**
* 参数校验
*/
private void validateSeckillParams(Long userId, Long activityId, Long goodsId, Integer quantity) {
if (ObjectUtils.isEmpty(userId)) {
throw new BusinessException(ErrorCodeEnum.USER_ID_NULL);
}
if (ObjectUtils.isEmpty(activityId)) {
throw new BusinessException(ErrorCodeEnum.ACTIVITY_ID_NULL);
}
if (ObjectUtils.isEmpty(goodsId)) {
throw new BusinessException(ErrorCodeEnum.GOODS_ID_NULL);
}
if (ObjectUtils.isEmpty(quantity) || quantity <= 0) {
throw new BusinessException(ErrorCodeEnum.QUANTITY_INVALID);
}
}
/**
* 检查活动状态
*/
private MoonCakeSeckillActivity checkActivityStatus(Long activityId) {
// 先从缓存获取活动信息
Cache cache = cacheManager.getCache(seckillProperties.getActivity().getCachePrefix());
MoonCakeSeckillActivity activity = cache.get(activityId, MoonCakeSeckillActivity.class);
// 缓存未命中,从数据库获取
if (ObjectUtils.isEmpty(activity)) {
activity = activityService.getById(activityId);
if (ObjectUtils.isEmpty(activity)) {
log.error("秒杀活动不存在,activityId: {}", activityId);
throw new BusinessException(ErrorCodeEnum.ACTIVITY_NOT_FOUND);
}
// 存入缓存
cache.put(activityId, activity);
}
// 检查活动状态
if (!ActivityStatusEnum.STARTED.getCode().equals(activity.getStatus())) {
log.error("秒杀活动状态异常,activityId: {}, status: {}", activityId, activity.getStatus());
if (ActivityStatusEnum.DRAFT.getCode().equals(activity.getStatus())) {
throw new BusinessException(ErrorCodeEnum.ACTIVITY_DRAFT);
} else if (ActivityStatusEnum.PUBLISHED.getCode().equals(activity.getStatus())) {
throw new BusinessException(ErrorCodeEnum.ACTIVITY_NOT_STARTED);
} else if (ActivityStatusEnum.ENDED.getCode().equals(activity.getStatus())) {
throw new BusinessException(ErrorCodeEnum.ACTIVITY_HAS_ENDED);
} else {
throw new BusinessException(ErrorCodeEnum.ACTIVITY_STATUS_INVALID);
}
}
// 再次检查时间(防止缓存中的活动信息未及时更新)
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activity.getStartTime())) {
log.error("秒杀活动尚未开始,activityId: {}, startTime: {}", activityId, activity.getStartTime());
throw new BusinessException(ErrorCodeEnum.ACTIVITY_NOT_STARTED);
}
if (now.isAfter(activity.getEndTime())) {
log.error("秒杀活动已结束,activityId: {}, endTime: {}", activityId, activity.getEndTime());
throw new BusinessException(ErrorCodeEnum.ACTIVITY_HAS_ENDED);
}
return activity;
}
/**
* 检查秒杀商品信息和库存
*/
private MoonCakeSeckillGoods checkSeckillGoods(Long activityId, Long goodsId, Integer quantity) {
LambdaQueryWrapper<MoonCakeSeckillGoods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MoonCakeSeckillGoods::getActivityId, activityId)
.eq(MoonCakeSeckillGoods::getGoodsId, goodsId);
MoonCakeSeckillGoods seckillGoods = seckillGoodsService.getOne(queryWrapper);
if (ObjectUtils.isEmpty(seckillGoods)) {
log.error("秒杀商品不存在,activityId: {}, goodsId: {}", activityId, goodsId);
throw new BusinessException(ErrorCodeEnum.SECKILL_GOODS_NOT_FOUND);
}
// 检查购买数量是否超过限购
if (quantity > seckillGoods.getPerUserLimit()) {
log.error("购买数量超过限购,userId: {}, goodsId: {}, quantity: {}, limit: {}",
null, goodsId, quantity, seckillGoods.getPerUserLimit());
throw new BusinessException(ErrorCodeEnum.OVER_PURCHASE_LIMIT);
}
return seckillGoods;
}
/**
* 检查用户参与条件
*/
private void checkUserConditions(Long userId, Long activityId, Long goodsId, Integer quantity,
MoonCakeSeckillActivity activity, MoonCakeSeckillGoods seckillGoods) {
// 检查用户是否已经达到限购数量
MoonCakeSeckillUserStat userStat = userStatService.getByUserActivityGoods(userId, activityId, goodsId);
int hasPurchased = ObjectUtils.isEmpty(userStat) ? 0 : userStat.getSuccessCount();
if (hasPurchased + quantity > seckillGoods.getPerUserLimit()) {
log.error("用户已超过限购数量,userId: {}, goodsId: {}, hasPurchased: {}, quantity: {}, limit: {}",
userId, goodsId, hasPurchased, quantity, seckillGoods.getPerUserLimit());
throw new BusinessException(ErrorCodeEnum.OVER_PURCHASE_LIMIT);
}
// 检查活动总参与人数限制
if (activity.getTotalPerson() > 0) {
int totalParticipants = userStatService.countTotalParticipants(activityId);
if (totalParticipants >= activity.getTotalPerson()) {
log.error("活动参与人数已达上限,activityId: {}, totalPerson: {}", activityId, activity.getTotalPerson());
throw new BusinessException(ErrorCodeEnum.ACTIVITY_PARTICIPANT_LIMIT);
}
}
}
/**
* 检查用户频率限制
*/
private void checkUserFrequency(Long userId, Long activityId, Long goodsId) {
String frequencyKey = seckillProperties.getUser().getLimitPrefix() + activityId + ":" + goodsId + ":" + userId;
// 检查用户最近一次参与时间,防止频繁点击
Object lastTimeObj = redisTemplate.opsForValue().get(frequencyKey);
if (!ObjectUtils.isEmpty(lastTimeObj)) {
try {
long lastTime = Long.parseLong(lastTimeObj.toString());
long currentTime = System.currentTimeMillis();
// 限制1秒内只能参与一次
if (currentTime - lastTime < 1000) {
log.warn("用户秒杀频率过高,userId: {}, activityId: {}, goodsId: {}", userId, activityId, goodsId);
throw new BusinessException(ErrorCodeEnum.OPERATE_TOO_FREQUENTLY);
}
} catch (NumberFormatException e) {
log.error("解析用户秒杀时间异常", e);
}
}
// 更新用户最后参与时间
redisTemplate.opsForValue().set(frequencyKey, System.currentTimeMillis(), 1, TimeUnit.MINUTES);
}
/**
* 扣减缓存库存
*/
private boolean deductStockInCache(Long activityId, Long goodsId, Integer quantity) {
String stockKey = seckillProperties.getStock().getCachePrefix() + activityId + ":" + goodsId;
// 使用Redis的decrement命令原子性扣减库存
Long remainingStock = redisTemplate.opsForValue().decrement(stockKey, quantity);
// 如果剩余库存 >= 0,说明扣减成功
if (remainingStock != null && remainingStock >= 0) {
log.debug("缓存库存扣减成功,activityId: {}, goodsId: {}, remaining: {}", activityId, goodsId, remainingStock);
return true;
}
// 如果扣减后库存为负数,需要回滚
if (remainingStock != null && remainingStock < 0) {
redisTemplate.opsForValue().increment(stockKey, quantity);
}
log.debug("缓存库存扣减失败,activityId: {}, goodsId: {}", activityId, goodsId);
return false;
}
/**
* 回滚缓存库存
*/
private void recoverStockInCache(Long activityId, Long goodsId, Integer quantity) {
String stockKey = seckillProperties.getStock().getCachePrefix() + activityId + ":" + goodsId;
redisTemplate.opsForValue().increment(stockKey, quantity);
log.debug("回滚缓存库存,activityId: {}, goodsId: {}, quantity: {}", activityId, goodsId, quantity);
}
/**
* 创建秒杀订单
*/
private MoonCakeSeckillOrder createSeckillOrder(Long userId, Long activityId,
MoonCakeSeckillGoods seckillGoods, Integer quantity) {
MoonCakeSeckillOrder order = new MoonCakeSeckillOrder();
// 生成唯一订单号
String orderNo = generateOrderNo(userId);
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setActivityId(activityId);
order.setGoodsId(seckillGoods.getGoodsId());
order.setGoodsName(seckillGoods.getGoodsName());
order.setSeckillPrice(seckillGoods.getSeckillPrice());
order.setQuantity(quantity);
// 计算总金额
BigDecimal totalAmount = seckillGoods.getSeckillPrice().multiply(new BigDecimal(quantity));
order.setTotalAmount(totalAmount);
// 设置订单状态为待支付
order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
// 设置订单过期时间(15分钟后)
order.setExpireTime(LocalDateTime.now().plusMinutes(seckillProperties.getOrder().getPayTimeout()));
order.setCreateTime(LocalDateTime.now());
order.setUpdateTime(LocalDateTime.now());
// 保存订单
baseMapper.insert(order);
log.info("创建秒杀订单成功,orderNo: {}, userId: {}, goodsId: {}", orderNo, userId, seckillGoods.getGoodsId());
return order;
}
/**
* 生成订单号
* 规则:时间戳(13位) + 用户ID后4位 + 随机数(4位)
*/
private String generateOrderNo(Long userId) {
StringBuilder orderNo = new StringBuilder();
// 时间戳
orderNo.append(System.currentTimeMillis());
// 用户ID后4位
String userIdStr = String.valueOf(userId);
if (userIdStr.length() >= 4) {
orderNo.append(userIdStr.substring(userIdStr.length() - 4));
} else {
orderNo.append(String.format("%04d", userId));
}
// 4位随机数
orderNo.append((int) ((Math.random() * 9000) + 1000));
return orderNo.toString();
}
/**
* 更新用户统计信息
*/
private void updateUserStat(Long userId, Long activityId, Long goodsId, boolean isSuccess) {
MoonCakeSeckillUserStat userStat = userStatService.getByUserActivityGoods(userId, activityId, goodsId);
if (ObjectUtils.isEmpty(userStat)) {
userStat = new MoonCakeSeckillUserStat();
userStat.setUserId(userId);
userStat.setActivityId(activityId);
userStat.setGoodsId(goodsId);
userStat.setTotalCount(1);
userStat.setSuccessCount(isSuccess ? 1 : 0);
userStat.setLastSeckillTime(LocalDateTime.now());
userStatService.save(userStat);
} else {
userStat.setTotalCount(userStat.getTotalCount() + 1);
if (isSuccess) {
userStat.setSuccessCount(userStat.getSuccessCount() + 1);
}
userStat.setLastSeckillTime(LocalDateTime.now());
userStatService.updateById(userStat);
}
log.debug("更新用户秒杀统计信息,userId: {}, activityId: {}, goodsId: {}", userId, activityId, goodsId);
}
/**
* 预热秒杀商品库存到缓存
*/
@Override
public void preloadStockToCache(Long activityId) {
if (ObjectUtils.isEmpty(activityId)) {
log.error("活动ID为空,无法预热库存");
return;
}
// 查询活动下的所有秒杀商品
LambdaQueryWrapper<MoonCakeSeckillGoods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MoonCakeSeckillGoods::getActivityId, activityId);
List<MoonCakeSeckillGoods> seckillGoodsList = seckillGoodsService.list(queryWrapper);
if (ObjectUtils.isEmpty(seckillGoodsList)) {
log.warn("活动下没有秒杀商品,无需预热库存,activityId: {}", activityId);
return;
}
// 将库存加载到Redis和本地缓存
for (MoonCakeSeckillGoods goods : seckillGoodsList) {
String stockKey = seckillProperties.getStock().getCachePrefix() + activityId + ":" + goods.getGoodsId();
// 设置Redis缓存,有效期为24小时
redisTemplate.opsForValue().set(stockKey, goods.getAvailableStock(), 24, TimeUnit.HOURS);
// 设置本地缓存
Cache cache = cacheManager.getCache(seckillProperties.getStock().getCachePrefix());
cache.put(activityId + ":" + goods.getGoodsId(), goods.getAvailableStock());
log.info("预热秒杀商品库存到缓存,activityId: {}, goodsId: {}, stock: {}",
activityId, goods.getGoodsId(), goods.getAvailableStock());
}
// 同时预热活动信息到缓存
MoonCakeSeckillActivity activity = activityService.getById(activityId);
if (!ObjectUtils.isEmpty(activity)) {
Cache cache = cacheManager.getCache(seckillProperties.getActivity().getCachePrefix());
cache.put(activityId, activity);
log.info("预热秒杀活动信息到缓存,activityId: {}", activityId);
}
}
/**
* 查询秒杀商品的实时库存
*/
@Override
public Integer getSeckillStock(Long activityId, Long goodsId) {
if (ObjectUtils.isEmpty(activityId) || ObjectUtils.isEmpty(goodsId)) {
log.error("活动ID或商品ID为空,无法查询库存");
return 0;
}
// 先从本地缓存查询
Cache cache = cacheManager.getCache(seckillProperties.getStock().getCachePrefix());
Integer localStock = cache.get(activityId + ":" + goodsId, Integer.class);
if (!ObjectUtils.isEmpty(localStock)) {
return Math.max(localStock, 0);
}
// 本地缓存未命中,从Redis查询
String stockKey = seckillProperties.getStock().getCachePrefix() + activityId + ":" + goodsId;
Object redisStockObj = redisTemplate.opsForValue().get(stockKey);
if (!ObjectUtils.isEmpty(redisStockObj)) {
try {
int redisStock = Integer.parseInt(redisStockObj.toString());
// 更新本地缓存
cache.put(activityId + ":" + goodsId, redisStock);
return Math.max(redisStock, 0);
} catch (NumberFormatException e) {
log.error("解析Redis库存异常", e);
}
}
// Redis也未命中,从数据库查询
LambdaQueryWrapper<MoonCakeSeckillGoods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MoonCakeSeckillGoods::getActivityId, activityId)
.eq(MoonCakeSeckillGoods::getGoodsId, goodsId)
.select(MoonCakeSeckillGoods::getAvailableStock);
MoonCakeSeckillGoods seckillGoods = seckillGoodsService.getOne(queryWrapper);
int dbStock = ObjectUtils.isEmpty(seckillGoods) ? 0 : seckillGoods.getAvailableStock();
// 更新Redis和本地缓存
redisTemplate.opsForValue().set(stockKey, dbStock, 1, TimeUnit.HOURS);
cache.put(activityId + ":" + goodsId, dbStock);
log.debug("查询秒杀商品库存,activityId: {}, goodsId: {}, stock: {}", activityId, goodsId, dbStock);
return Math.max(dbStock, 0);
}
/**
* 处理超时未支付的订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void handleTimeoutOrder(String orderNo) {
if (ObjectUtils.isEmpty(orderNo)) {
log.error("订单编号为空,无法处理超时订单");
return;
}
// 查询订单
LambdaQueryWrapper<MoonCakeSeckillOrder> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MoonCakeSeckillOrder::getOrderNo, orderNo);
MoonCakeSeckillOrder order = getOne(queryWrapper);
if (ObjectUtils.isEmpty(order)) {
log.warn("订单不存在,orderNo: {}", orderNo);
return;
}
// 检查订单状态,只有待支付状态的订单才需要处理
if (!OrderStatusEnum.PENDING_PAYMENT.getCode().equals(order.getStatus())) {
log.info("订单状态不是待支付,无需处理超时,orderNo: {}, status: {}", orderNo, order.getStatus());
return;
}
// 检查是否真的超时
if (LocalDateTime.now().isBefore(order.getExpireTime())) {
log.info("订单尚未超时,无需处理,orderNo: {}, expireTime: {}", orderNo, order.getExpireTime());
return;
}
// 更新订单状态为已超时
LambdaUpdateWrapper<MoonCakeSeckillOrder> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MoonCakeSeckillOrder::getOrderNo, orderNo)
.eq(MoonCakeSeckillOrder::getStatus, OrderStatusEnum.PENDING_PAYMENT.getCode())
.set(MoonCakeSeckillOrder::getStatus, OrderStatusEnum.TIMEOUT.getCode())
.set(MoonCakeSeckillOrder::getUpdateTime, LocalDateTime.now());
boolean updateSuccess = update(updateWrapper);
if (!updateSuccess) {
log.error("更新超时订单状态失败,orderNo: {}", orderNo);
return;
}
log.info("订单已超时,更新状态成功,orderNo: {}", orderNo);
// 恢复库存
boolean recoverSuccess = seckillGoodsService.recoverStockByActivityAndGoods(
order.getActivityId(), order.getGoodsId(), order.getQuantity());
if (recoverSuccess) {
// 同时恢复缓存中的库存
recoverStockInCache(order.getActivityId(), order.getGoodsId(), order.getQuantity());
log.info("超时订单库存已恢复,orderNo: {}, goodsId: {}, quantity: {}",
orderNo, order.getGoodsId(), order.getQuantity());
} else {
log.error("超时订单库存恢复失败,orderNo: {}, goodsId: {}, quantity: {}",
orderNo, order.getGoodsId(), order.getQuantity());
}
// 发送订单超时消息
Map<String, Object> message = Maps.newHashMap();
message.put("orderNo", orderNo);
message.put("userId", order.getUserId());
message.put("goodsId", order.getGoodsId());
message.put("reason", "支付超时");
messageProducer.sendOrderCanceledMessage(message);
}
}
库存管理是秒杀系统的核心,直接关系到是否会出现超卖问题:
package com.mooncake.seckill.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mooncake.seckill.entity.MoonCakeSeckillGoods;
/**
* 月饼秒杀商品服务接口
*
* @author 果酱
*/
public interface MoonCakeSeckillGoodsService extends IService<MoonCakeSeckillGoods> {
/**
* 扣减库存
*
* @param id 秒杀商品ID
* @param quantity 扣减数量
* @return 影响行数
*/
int deductStock(Long id, Integer quantity);
/**
* 恢复库存
*
* @param id 秒杀商品ID
* @param quantity 恢复数量
* @return 影响行数
*/
int recoverStock(Long id, Integer quantity);
/**
* 根据活动和商品恢复库存
*
* @param activityId 活动ID
* @param goodsId 商品ID
* @param quantity 恢复数量
* @return 是否成功
*/
boolean recoverStockByActivityAndGoods(Long activityId, Long goodsId, Integer quantity);
}
package com.mooncake.seckill.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mooncake.seckill.entity.MoonCakeSeckillGoods;
import com.mooncake.seckill.mapper.MoonCakeSeckillGoodsMapper;
import com.mooncake.seckill.service.MoonCakeSeckillGoodsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
/**
* 月饼秒杀商品服务实现类
*
* @author 果酱
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MoonCakeSeckillGoodsServiceImpl extends ServiceImpl<MoonCakeSeckillGoodsMapper, MoonCakeSeckillGoods> implements MoonCakeSeckillGoodsService {
private final MoonCakeSeckillGoodsMapper seckillGoodsMapper;
/**
* 扣减库存
*
* @param id 秒杀商品ID
* @param quantity 扣减数量
* @return 影响行数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deductStock(Long id, Integer quantity) {
if (ObjectUtils.isEmpty(id) || ObjectUtils.isEmpty(quantity) || quantity <= 0) {
log.error("扣减库存参数无效,id: {}, quantity: {}", id, quantity);
return 0;
}
// 扣减库存,注意where条件中增加库存大于等于扣减数量的判断,防止超卖
int affectRows = seckillGoodsMapper.deductStock(id, quantity);
log.debug("扣减库存,id: {}, quantity: {}, affectRows: {}", id, quantity, affectRows);
return affectRows;
}
/**
* 恢复库存
*
* @param id 秒杀商品ID
* @param quantity 恢复数量
* @return 影响行数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int recoverStock(Long id, Integer quantity) {
if (ObjectUtils.isEmpty(id) || ObjectUtils.isEmpty(quantity) || quantity <= 0) {
log.error("恢复库存参数无效,id: {}, quantity: {}", id, quantity);
return 0;
}
int affectRows = seckillGoodsMapper.recoverStock(id, quantity);
log.debug("恢复库存,id: {}, quantity: {}, affectRows: {}", id, quantity, affectRows);
return affectRows;
}
/**
* 根据活动和商品恢复库存
*
* @param activityId 活动ID
* @param goodsId 商品ID
* @param quantity 恢复数量
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean recoverStockByActivityAndGoods(Long activityId, Long goodsId, Integer quantity) {
if (ObjectUtils.isEmpty(activityId) || ObjectUtils.isEmpty(goodsId) ||
ObjectUtils.isEmpty(quantity) || quantity <= 0) {
log.error("恢复库存参数无效,activityId: {}, goodsId: {}, quantity: {}", activityId, goodsId, quantity);
return false;
}
// 查询秒杀商品
LambdaQueryWrapper<MoonCakeSeckillGoods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MoonCakeSeckillGoods::getActivityId, activityId)
.eq(MoonCakeSeckillGoods::getGoodsId, goodsId);
MoonCakeSeckillGoods seckillGoods = getOne(queryWrapper);
if (ObjectUtils.isEmpty(seckillGoods)) {
log.error("秒杀商品不存在,无法恢复库存,activityId: {}, goodsId: {}", activityId, goodsId);
return false;
}
// 恢复库存
LambdaUpdateWrapper<MoonCakeSeckillGoods> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MoonCakeSeckillGoods::getId, seckillGoods.getId())
.setSql("available_stock = available_stock + " + quantity)
.set(MoonCakeSeckillGoods::getUpdateTime, java.time.LocalDateTime.now());
boolean updateSuccess = update(updateWrapper);
log.debug("根据活动和商品恢复库存,activityId: {}, goodsId: {}, quantity: {}, success: {}",
activityId, goodsId, quantity, updateSuccess);
return updateSuccess;
}
}
对应的 Mapper XML 文件(MoonCakeSeckillGoodsMapper.xml):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mooncake.seckill.mapper.MoonCakeSeckillGoodsMapper">
<!-- 扣减库存 -->
<update id="deductStock">
UPDATE moon_cake_seckill_goods
SET available_stock = available_stock - #{quantity},
update_time = NOW()
WHERE id = #{id}
AND available_stock >= #{quantity}
</update>
<!-- 恢复库存 -->
<update id="recoverStock">
UPDATE moon_cake_seckill_goods
SET available_stock = available_stock + #{quantity},
update_time = NOW()
WHERE id = #{id}
</update>
</mapper>
控制器负责接收前端请求,调用服务层处理,并返回结果:
package com.mooncake.seckill.controller;
import com.mooncake.seckill.common.Result;
import com.mooncake.seckill.service.SeckillService;
import com.mooncake.seckill.vo.SeckillResultVO;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 秒杀控制器
*
* @author 果酱
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/seckill")
@RequiredArgsConstructor
@Tag(name = "月饼秒杀接口", description = "提供月饼秒杀相关的接口")
public class SeckillController {
private final SeckillService seckillService;
/**
* 执行秒杀
*/
@PostMapping("/do")
@Operation(summary = "执行秒杀", description = "用户参与月饼秒杀活动")
@RateLimiter(name = "seckillApi", fallbackMethod = "seckillFallback")
public Result<SeckillResultVO> doSeckill(
@Parameter(description = "用户ID", required = true)
@RequestParam Long userId,
@Parameter(description = "活动ID", required = true)
@RequestParam Long activityId,
@Parameter(description = "商品ID", required = true)
@RequestParam Long goodsId,
@Parameter(description = "购买数量", required = true)
@RequestParam(defaultValue = "1") Integer quantity) {
log.info("用户发起秒杀请求,userId: {}, activityId: {}, goodsId: {}, quantity: {}",
userId, activityId, goodsId, quantity);
SeckillResultVO result = seckillService.doSeckill(userId, activityId, goodsId, quantity);
return Result.success(result);
}
/**
* 秒杀接口限流降级处理
*/
public Result<SeckillResultVO> seckillFallback(Long userId, Long activityId, Long goodsId, Integer quantity, Exception e) {
log.warn("秒杀接口触发限流,userId: {}, activityId: {}, goodsId: {}", userId, activityId, goodsId, e);
return Result.fail("当前参与人数过多,请稍后再试");
}
/**
* 获取秒杀商品库存
*/
@GetMapping("/stock")
@Operation(summary = "获取秒杀商品库存", description = "查询指定活动中商品的实时库存")
public Result<Integer> getSeckillStock(
@Parameter(description = "活动ID", required = true)
@RequestParam Long activityId,
@Parameter(description = "商品ID", required = true)
@RequestParam Long goodsId) {
log.info("查询秒杀商品库存,activityId: {}, goodsId: {}", activityId, goodsId);
Integer stock = seckillService.getSeckillStock(activityId, goodsId);
return Result.success(stock);
}
/**
* 预热秒杀库存到缓存
* 注意:实际生产环境中,这个接口应该有严格的权限控制
*/
@PostMapping("/preload/{activityId}")
@Operation(summary = "预热秒杀库存到缓存", description = "将指定活动的商品库存预热到缓存中")
public Result<Void> preloadStock(
@Parameter(description = "活动ID", required = true)
@PathVariable Long activityId) {
log.info("开始预热秒杀库存到缓存,activityId: {}", activityId);
seckillService.preloadStockToCache(activityId);
return Result.success();
}
}
使用 RabbitMQ 处理异步任务,如订单超时处理、通知等:
package com.mooncake.seckill.mq.producer;
import com.alibaba.fastjson2.JSON;
import com.mooncake.seckill.constant.MqConstant;
import com.mooncake.seckill.vo.UserSeckillVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.Map;
import java.util.UUID;
/**
* 秒杀消息生产者
*
* @author 果酱
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillMessageProducer {
private final RabbitTemplate rabbitTemplate;
/**
* 发送订单创建消息
*/
public void sendOrderCreatedMessage(UserSeckillVO userSeckillVO) {
if (ObjectUtils.isEmpty(userSeckillVO)) {
log.error("发送订单创建消息失败,消息内容为空");
return;
}
try {
// 生成消息ID
String messageId = UUID.randomUUID().toString();
CorrelationData correlationData = new CorrelationData(messageId);
// 发送消息
rabbitTemplate.convertAndSend(
MqConstant.SECKILL_EXCHANGE,
MqConstant.ORDER_CREATED_ROUTING_KEY,
JSON.toJSONString(userSeckillVO),
correlationData
);
log.info("发送订单创建消息成功,messageId: {}, orderNo: {}", messageId, userSeckillVO.getOrderNo());
} catch (Exception e) {
log.error("发送订单创建消息失败,userSeckillVO: {}", JSON.toJSONString(userSeckillVO), e);
// 实际项目中可以考虑重试或存入本地消息表
}
}
/**
* 发送订单取消消息
*/
public void sendOrderCanceledMessage(Map<String, Object> message) {
if (ObjectUtils.isEmpty(message)) {
log.error("发送订单取消消息失败,消息内容为空");
return;
}
try {
// 生成消息ID
String messageId = UUID.randomUUID().toString();
CorrelationData correlationData = new CorrelationData(messageId);
// 发送消息
rabbitTemplate.convertAndSend(
MqConstant.SECKILL_EXCHANGE,
MqConstant.ORDER_CANCELED_ROUTING_KEY,
JSON.toJSONString(message),
correlationData
);
log.info("发送订单取消消息成功,messageId: {}, orderNo: {}", messageId, message.get("orderNo"));
} catch (Exception e) {
log.error("发送订单取消消息失败,message: {}", JSON.toJSONString(message), e);
}
}
}
package com.mooncake.seckill.mq.consumer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.mooncake.seckill.constant.MqConstant;
import com.mooncake.seckill.entity.MoonCakeSeckillOrder;
import com.mooncake.seckill.enums.OrderStatusEnum;
import com.mooncake.seckill.service.MoonCakeSeckillOrderService;
import com.mooncake.seckill.service.NotifyService;
import com.mooncake.seckill.vo.UserSeckillVO;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 订单消息消费者
*
* @author 果酱
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderMessageConsumer {
private final NotifyService notifyService;
private final MoonCakeSeckillOrderService orderService;
/**
* 处理订单创建消息,发送订单通知并设置支付超时检查
*/
@RabbitListener(queues = MqConstant.ORDER_CREATED_QUEUE)
public void handleOrderCreatedMessage(String message, Channel channel, Message amqpMessage) throws IOException {
log.info("收到订单创建消息: {}", message);
try {
// 解析消息
UserSeckillVO userSeckillVO = JSON.parseObject(message, new TypeReference<UserSeckillVO>() {});
if (ObjectUtils.isEmpty(userSeckillVO)) {
log.error("订单创建消息解析失败,消息内容为空");
// 确认消息已消费
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
return;
}
// 发送订单创建通知(短信/推送)
notifyService.sendOrderCreatedNotify(userSeckillVO);
log.info("订单创建通知发送成功,orderNo: {}", userSeckillVO.getOrderNo());
// 确认消息已消费
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("处理订单创建消息异常", e);
// 消息处理失败,拒绝消息并将其重新入队
channel.basicNack(amqpMessage.getMessageProperties().getDeliveryTag(), false, true);
}
}
/**
* 处理订单取消消息,发送订单取消通知
*/
@RabbitListener(queues = MqConstant.ORDER_CANCELED_QUEUE)
public void handleOrderCanceledMessage(String message, Channel channel, Message amqpMessage) throws IOException {
log.info("收到订单取消消息: {}", message);
try {
// 解析消息
Map<String, Object> messageMap = JSON.parseObject(message, new TypeReference<Map<String, Object>>() {});
if (ObjectUtils.isEmpty(messageMap) || ObjectUtils.isEmpty(messageMap.get("orderNo"))) {
log.error("订单取消消息解析失败,消息内容无效");
// 确认消息已消费
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
return;
}
String orderNo = messageMap.get("orderNo").toString();
Long userId = Long.parseLong(messageMap.get("userId").toString());
String reason = messageMap.get("reason").toString();
// 发送订单取消通知
notifyService.sendOrderCanceledNotify(orderNo, userId, reason);
log.info("订单取消通知发送成功,orderNo: {}", orderNo);
// 确认消息已消费
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("处理订单取消消息异常", e);
// 消息处理失败,拒绝消息并将其重新入队
channel.basicNack(amqpMessage.getMessageProperties().getDeliveryTag(), false, true);
}
}
/**
* 处理订单支付超时检查消息
*/
@RabbitListener(queues = MqConstant.ORDER_PAY_TIMEOUT_QUEUE)
public void handleOrderPayTimeoutMessage(String message, Channel channel, Message amqpMessage) throws IOException {
log.info("收到订单支付超时检查消息: {}", message);
try {
// 解析消息
Map<String, Object> messageMap = JSON.parseObject(message, new TypeReference<Map<String, Object>>() {});
if (ObjectUtils.isEmpty(messageMap) || ObjectUtils.isEmpty(messageMap.get("orderNo"))) {
log.error("订单支付超时检查消息解析失败,消息内容无效");
// 确认消息已消费
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
return;
}
String orderNo = messageMap.get("orderNo").toString();
// 查询订单状态
MoonCakeSeckillOrder order = orderService.getByOrderNo(orderNo);
if (ObjectUtils.isEmpty(order)) {
log.warn("订单不存在,无需处理支付超时,orderNo: {}", orderNo);
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
return;
}
// 检查订单状态,如果还是待支付状态,则处理超时
if (OrderStatusEnum.PENDING_PAYMENT.getCode().equals(order.getStatus())) {
log.info("订单仍未支付,处理超时逻辑,orderNo: {}", orderNo);
orderService.handleTimeoutOrder(orderNo);
} else {
log.info("订单状态已变更,无需处理支付超时,orderNo: {}, status: {}", orderNo, order.getStatus());
}
// 确认消息已消费
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("处理订单支付超时消息异常", e);
// 消息处理失败,拒绝消息并将其重新入队
channel.basicNack(amqpMessage.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
月饼秒杀系统的核心挑战在于处理高并发请求,同时保证数据一致性和系统稳定性。以下是关键的优化策略:
/**
* 本地缓存配置
*/
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 配置不同缓存的过期时间
cacheManager.setCacheSpecification(
"seckill:activity=600s " +
"seckill:stock=60s " +
"seckill:goods=300s"
);
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(10000)
.recordStats();
}
}
/**
* 用户级限流切面
*/
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class UserRateLimitAspect {
private final RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.mooncake.seckill.annotation.UserRateLimit)")
public void userRateLimitPointcut() {}
@Around("userRateLimitPointcut() && @annotation(limit)")
public Object around(ProceedingJoinPoint joinPoint, UserRateLimit limit) throws Throwable {
// 获取用户ID参数(假设第一个参数是userId)
Object[] args = joinPoint.getArgs();
if (ObjectUtils.isEmpty(args) || !(args[0] instanceof Long)) {
log.error("用户限流切面获取用户ID失败");
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR);
}
Long userId = (Long) args[0];
String limitKey = "user:rate:limit:" + userId + ":" + joinPoint.getSignature().getName();
// 使用Redis实现滑动窗口限流
Long currentTime = System.currentTimeMillis();
Long windowSize = (long) limit.windowSeconds() * 1000;
// 移除窗口外的数据
redisTemplate.opsForZSet().removeRangeByScore(limitKey, 0, currentTime - windowSize);
// 统计窗口内的请求数
Long count = redisTemplate.opsForZSet().zCard(limitKey);
if (count >= limit.maxCount()) {
log.warn("用户触发限流,userId: {}, count: {}, maxCount: {}", userId, count, limit.maxCount());
throw new BusinessException(ErrorCodeEnum.OPERATE_TOO_FREQUENTLY);
}
// 添加当前请求时间戳
redisTemplate.opsForZSet().add(limitKey, UUID.randomUUID().toString(), currentTime);
// 设置过期时间
redisTemplate.expire(limitKey, limit.windowSeconds() + 1, TimeUnit.SECONDS);
// 执行目标方法
return joinPoint.proceed();
}
}
超卖是秒杀系统中最需要避免的问题,我们通过多层防护确保库存安全:
/**
* 防超卖核心SQL解析
*/
// 这条SQL是防止超卖的最后一道防线
// 只有当可用库存大于等于要扣减的数量时,才会执行更新操作
// 即使前面的缓存检查失效,这条SQL也能保证不会出现超卖
UPDATE moon_cake_seckill_goods
SET available_stock = available_stock - #{quantity},
update_time = NOW()
WHERE id = #{id}
AND available_stock >= #{quantity}
将非核心流程异步化,提高系统响应速度和吞吐量:
使用 Spring Boot Actuator + Prometheus + Grafana 实现系统监控:
<!-- pom.xml添加监控依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml配置
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
export:
prometheus:
enabled: true
tags:
application: moon-cake-seckill
endpoint:
health:
show-details: always
probes:
enabled: true
自定义业务指标:
@Component
@Slf4j
public class SeckillMetrics {
private final MeterRegistry meterRegistry;
// 秒杀总次数计数器
private final Counter seckillTotalCounter;
// 秒杀成功计数器
private final Counter seckillSuccessCounter;
// 秒杀失败计数器
private final Counter seckillFailCounter;
// 秒杀响应时间计时器
private final Timer seckillTimer;
@Autowired
public SeckillMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.seckillTotalCounter = Counter.builder("seckill.total")
.description("秒杀总次数")
.register(meterRegistry);
this.seckillSuccessCounter = Counter.builder("seckill.success")
.description("秒杀成功次数")
.register(meterRegistry);
this.seckillFailCounter = Counter.builder("seckill.fail")
.description("秒杀失败次数")
.register(meterRegistry);
this.seckillTimer = Timer.builder("seckill.response.time")
.description("秒杀响应时间")
.register(meterRegistry);
}
// 记录秒杀开始
public Timer.Sample startSeckill() {
seckillTotalCounter.increment();
return Timer.start(meterRegistry);
}
// 记录秒杀成功
public void recordSuccess(Timer.Sample sample) {
seckillSuccessCounter.increment();
sample.stop(seckillTimer);
}
// 记录秒杀失败
public void recordFail(Timer.Sample sample) {
seckillFailCounter.increment();
sample.stop(seckillTimer);
}
}
中秋月饼秒杀系统作为一种典型的高并发场景,其设计与实现涉及到分布式系统、高并发处理、数据一致性等多个技术领域的知识。本文从需求分析、架构设计、数据库设计、核心代码实现到高并发处理,全面介绍了一个专业月饼秒杀系统的构建过程。