首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >中秋月饼秒杀系统设计:从 0 到 1 构建高并发抢购平台

中秋月饼秒杀系统设计:从 0 到 1 构建高并发抢购平台

作者头像
果酱带你啃java
发布2026-04-14 13:16:11
发布2026-04-14 13:16:11
210
举报

引言:为什么需要专业的月饼秒杀系统?

中秋佳节临近,月饼作为传统节庆食品,成为各大电商平台和线下商家的营销焦点。而 "月饼秒杀" 活动,以其限时、限量、低价的特点,成为吸引用户、提升销量的重要手段。

然而,看似简单的 "秒杀" 背后,隐藏着巨大的技术挑战:当数万甚至数百万用户同时抢购限量的月饼礼盒时,如何保证系统不崩溃?如何防止超卖?如何确保公平性?如何处理突发流量?

本文将以中秋月饼秒杀为背景,从需求分析到架构设计,从核心算法到代码实现,全方位解析一个高并发秒杀系统的构建过程。无论你是电商平台的技术负责人,还是希望了解高并发系统设计的开发者,本文都将为你提供实用的技术参考和实践指导。

一、月饼秒杀系统需求分析

在开始设计之前,我们首先需要明确月饼秒杀系统的核心需求。一个完善的月饼秒杀系统应该具备以下功能和特性:

1.1 核心功能需求

  • 活动管理:支持创建、编辑、上下架月饼秒杀活动,设置活动时间、参与条件等
  • 商品管理:管理参与秒杀的月饼商品,包括库存设置、价格设置、限购数量等
  • 用户抢购:核心功能,支持用户在秒杀开始时抢购月饼
  • 订单处理:生成订单、支付超时处理、订单状态管理
  • 库存管理:实时库存扣减,防止超卖
  • 防作弊机制:防止恶意抢购、刷单等行为
  • 数据统计:提供参与人数、抢购成功率、销售额等统计数据

1.2 非功能需求

  • 高并发支持:秒杀活动通常在短时间内产生巨大流量,需要系统能够处理数万甚至数十万的并发请求
  • 高可用性:确保秒杀过程中系统稳定运行,避免因流量过大而崩溃
  • 一致性:严格保证库存一致性,绝对禁止超卖
  • 公平性:确保所有用户在秒杀面前机会均等,防止黄牛通过技术手段抢占优势
  • 实时性:库存状态、订单状态实时更新
  • 可扩展性:支持根据业务需求快速扩展功能

1.3 业务场景特点

月饼秒杀活动具有以下独特的业务特点:

  1. 时间集中:通常在特定时间点开始,如 "9 月 10 日 10:00 准时开抢"
  2. 流量暴增:秒杀开始瞬间会产生数十倍甚至上百倍于平时的流量
  3. 库存有限:热门月饼礼盒通常限量发售,库存较少
  4. 时效性强:秒杀活动持续时间短,通常几分钟内就会售罄
  5. 社会关注度高:中秋作为传统节日,月饼秒杀活动往往受到用户高度关注,系统故障会造成较大负面影响

二、月饼秒杀系统架构设计

基于上述需求分析,我们设计如下月饼秒杀系统架构:

2.1 架构说明

  1. 前端层
    • 用户端:包括 Web 页面、移动端 App、小程序等
    • CDN:静态资源加速,减轻应用服务器压力
  2. 接入层
    • 负载均衡:将请求分发到多个应用服务器,提高系统吞吐量
    • API 网关:统一入口,负责路由、认证、限流、监控等
  3. 业务服务层
    • 秒杀服务:核心服务,协调各子服务完成秒杀流程
    • 活动管理服务:管理秒杀活动的生命周期
    • 商品服务:管理月饼商品信息
    • 订单服务:处理订单创建、状态更新等
    • 库存服务:核心服务,负责库存扣减和防超卖
    • 支付服务:处理支付相关逻辑
    • 通知服务:负责订单通知、支付通知等
  4. 存储层
    • 关系型数据库:存储活动、商品、订单等核心数据
    • Redis 集群:缓存热点数据、库存计数器、分布式锁等
    • 本地缓存:应用服务器本地缓存,进一步减轻 Redis 压力
  5. 中间件
    • 消息队列:异步处理订单创建、支付通知、库存更新等非实时流程
    • 监控系统:监控系统运行状态和性能指标

2.2 核心流程设计

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

三、数据库设计

基于 MySQL 8.0 设计如下数据库表结构:

3.1 秒杀活动表(moon_cake_seckill_activity)

代码语言:javascript
复制
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='月饼秒杀活动表';
代码语言:javascript
复制

3.2 秒杀商品表(moon_cake_seckill_goods)

代码语言:javascript
复制
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='月饼秒杀商品表';
代码语言:javascript
复制

3.3 秒杀订单表(moon_cake_seckill_order)

代码语言:javascript
复制
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='月饼秒杀订单表';
代码语言:javascript
复制

3.4 用户秒杀记录统计表(moon_cake_seckill_user_stat)

代码语言:javascript
复制
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='用户秒杀记录统计表';
代码语言:javascript
复制

四、核心技术选型

基于需求分析和架构设计,我们选择以下技术栈:

  1. 基础框架:Spring Boot 3.2.0
  2. ORM 框架:MyBatis-Plus 3.5.5
  3. 数据库:MySQL 8.0(主从架构)
  4. 缓存:Redis 7.2.3(集群模式)
  5. 消息队列:RabbitMQ 3.13.0
  6. API 文档:Swagger3 (SpringDoc OpenAPI 2.1.0)
  7. 日志框架:SLF4J + Logback
  8. Lombok:1.18.30(简化代码)
  9. JSON 处理:Fastjson2 2.0.41
  10. 集合工具:Guava 32.1.3-jre
  11. 分布式锁:Redisson 3.23.3
  12. 限流工具:Resilience4j 2.1.0
  13. 任务调度:Quartz 2.3.2
  14. 测试框架:JUnit 5 + Mockito 5.8.0

五、项目初始化与配置

5.1 Maven 依赖配置(pom.xml)

代码语言:javascript
复制
<?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>
代码语言:javascript
复制

5.2 核心配置文件(application.yml)

代码语言:javascript
复制
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:"
代码语言:javascript
复制

六、核心代码实现

6.1 实体类设计

首先,我们定义核心实体类,对应数据库表结构:

6.1.1 秒杀活动实体(MoonCakeSeckillActivity.java)
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

6.1.2 秒杀商品实体(MoonCakeSeckillGoods.java)
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

6.1.3 秒杀订单实体(MoonCakeSeckillOrder.java)
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

6.1.4 用户秒杀记录统计实体(MoonCakeSeckillUserStat.java)
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

6.2 数据访问层(Mapper)

使用 MyBatis-Plus 的 BaseMapper 简化数据访问层代码:

6.2.1 秒杀活动 Mapper(MoonCakeSeckillActivityMapper.java)
代码语言:javascript
复制
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> {
}
代码语言:javascript
复制

其他实体的 Mapper 类实现类似,这里省略。

6.2.2 库存操作 Mapper 扩展(MoonCakeSeckillGoodsMapper.java)
代码语言:javascript
复制
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);
}
代码语言:javascript
复制

6.3 服务层设计

服务层是秒杀系统的核心,包含了业务逻辑的实现。我们采用分层设计,将服务分为接口和实现:

6.3.1 秒杀服务接口(SeckillService.java)
代码语言:javascript
复制
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);
}
代码语言:javascript
复制

6.3.2 秒杀服务实现(SeckillServiceImpl.java)
代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

6.4 库存服务设计

库存管理是秒杀系统的核心,直接关系到是否会出现超卖问题:

6.4.1 库存服务接口(MoonCakeSeckillGoodsService.java)
代码语言:javascript
复制
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);
}
代码语言:javascript
复制

6.4.2 库存服务实现(MoonCakeSeckillGoodsServiceImpl.java)
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

对应的 Mapper XML 文件(MoonCakeSeckillGoodsMapper.xml):

代码语言:javascript
复制
<?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>
代码语言:javascript
复制

6.5 控制器设计

控制器负责接收前端请求,调用服务层处理,并返回结果:

6.5.1 秒杀控制器(SeckillController.java)
代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

6.6 消息队列设计

使用 RabbitMQ 处理异步任务,如订单超时处理、通知等:

6.6.1 消息生产者(SeckillMessageProducer.java)
代码语言:javascript
复制
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);
        }
    }
}
代码语言:javascript
复制

6.6.2 消息消费者(OrderMessageConsumer.java)
代码语言:javascript
复制
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);
        }
    }
}
代码语言:javascript
复制

6.6.3 MQ 配置类(RabbitMqConfig.java)
代码语言:javascript
复制


七、高并发处理与性能优化

月饼秒杀系统的核心挑战在于处理高并发请求,同时保证数据一致性和系统稳定性。以下是关键的优化策略:

7.1 多级缓存架构

  1. 本地缓存:使用 Caffeine 作为本地缓存,存储活动信息、商品信息等不经常变化的数据,减少分布式缓存访问
  2. Redis 缓存:存储库存信息、用户参与记录等需要全局共享的数据
  3. 缓存预热:活动开始前将库存等数据加载到缓存,避免活动开始时的缓存穿透
代码语言:javascript
复制
/**
 * 本地缓存配置
 */
@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();
    }
}
代码语言:javascript
复制

7.2 流量控制

  1. 前端限流
    • 按钮置灰:用户点击后暂时置灰,防止重复点击
    • 验证码:添加图形验证码或滑块验证,增加请求成本
    • 排队机制:前端显示排队状态,分散用户请求
  2. 后端限流
    • 接口限流:使用 Resilience4j 实现接口级别的限流
    • 用户限流:限制单个用户单位时间内的请求次数
    • 活动限流:根据活动承载能力设置总请求上限
代码语言:javascript
复制
/**
 * 用户级限流切面
 */
@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();
    }
}
代码语言:javascript
复制

7.3 防超卖机制

超卖是秒杀系统中最需要避免的问题,我们通过多层防护确保库存安全:

  1. 前端防护:显示实时库存,库存为 0 时隐藏秒杀按钮
  2. 缓存层防护:使用 Redis 的 decrement 原子操作预扣减库存
  3. 数据库防护:UPDATE 语句添加库存条件判断(WHERE available_stock >= quantity)
  4. 分布式锁:关键操作加锁,确保并发安全
代码语言:javascript
复制
/**
 * 防超卖核心SQL解析
 */
// 这条SQL是防止超卖的最后一道防线
// 只有当可用库存大于等于要扣减的数量时,才会执行更新操作
// 即使前面的缓存检查失效,这条SQL也能保证不会出现超卖
UPDATE moon_cake_seckill_goods
SET available_stock = available_stock - #{quantity},
    update_time = NOW()
WHERE id = #{id}
  AND available_stock >= #{quantity}
代码语言:javascript
复制

7.4 异步化处理

将非核心流程异步化,提高系统响应速度和吞吐量:

  1. 订单创建后处理:订单创建成功后,通过 MQ 异步发送通知、设置支付超时检查
  2. 库存更新:缓存库存与数据库库存最终一致性通过异步任务保证
  3. 统计分析:用户参与、中奖等数据统计异步处理

7.5 数据库优化

  1. 索引优化:为活动 ID、商品 ID、用户 ID 等查询频繁的字段建立索引
  2. 分库分表:订单表等大数据量表采用分库分表策略
  3. 读写分离:主库负责写操作,从库负责读操作
  4. SQL 优化:避免复杂 SQL,减少锁竞争

八、系统监控与运维

8.1 关键监控指标

  1. 业务指标
    • 秒杀参与人数
    • 订单转化率
    • 支付成功率
    • 各商品抢购进度
    • 超时订单数量
  2. 技术指标
    • 接口响应时间(P99、P95、P50)
    • 接口调用量和错误率
    • 数据库连接数和慢查询数
    • Redis 内存使用和命中率
    • 消息队列堆积数
    • JVM 内存使用和 GC 情况

8.2 监控实现

使用 Spring Boot Actuator + Prometheus + Grafana 实现系统监控:

代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制

代码语言:javascript
复制
# 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
代码语言:javascript
复制

自定义业务指标:

代码语言:javascript
复制
@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);
    }
}
代码语言:javascript
复制

8.3 应急预案

  1. 流量突增应对
    • 快速扩容:准备弹性扩容机制,根据流量自动扩容
    • 降级措施:暂时关闭非核心功能,保障秒杀主流程
    • 限流调整:动态调整限流参数,允许更多用户参与
  2. 系统故障应对
    • 快速切换:准备备用系统,主系统故障时快速切换
    • 数据备份:定期备份数据,确保数据可恢复
    • 人工干预:准备应急操作手册,必要时人工介入
  3. 业务异常应对
    • 库存异常:发现库存异常时,立即暂停活动
    • 订单异常:批量处理异常订单的机制
    • 用户投诉:快速响应和处理用户投诉的流程

九、总结与展望

中秋月饼秒杀系统作为一种典型的高并发场景,其设计与实现涉及到分布式系统、高并发处理、数据一致性等多个技术领域的知识。本文从需求分析、架构设计、数据库设计、核心代码实现到高并发处理,全面介绍了一个专业月饼秒杀系统的构建过程。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言:为什么需要专业的月饼秒杀系统?
  • 一、月饼秒杀系统需求分析
    • 1.1 核心功能需求
    • 1.2 非功能需求
    • 1.3 业务场景特点
  • 二、月饼秒杀系统架构设计
    • 2.1 架构说明
    • 2.2 核心流程设计
  • 三、数据库设计
    • 3.1 秒杀活动表(moon_cake_seckill_activity)
    • 3.2 秒杀商品表(moon_cake_seckill_goods)
    • 3.3 秒杀订单表(moon_cake_seckill_order)
    • 3.4 用户秒杀记录统计表(moon_cake_seckill_user_stat)
  • 四、核心技术选型
  • 五、项目初始化与配置
    • 5.1 Maven 依赖配置(pom.xml)
    • 5.2 核心配置文件(application.yml)
  • 六、核心代码实现
    • 6.1 实体类设计
      • 6.1.1 秒杀活动实体(MoonCakeSeckillActivity.java)
      • 6.1.2 秒杀商品实体(MoonCakeSeckillGoods.java)
      • 6.1.3 秒杀订单实体(MoonCakeSeckillOrder.java)
      • 6.1.4 用户秒杀记录统计实体(MoonCakeSeckillUserStat.java)
    • 6.2 数据访问层(Mapper)
      • 6.2.1 秒杀活动 Mapper(MoonCakeSeckillActivityMapper.java)
      • 6.2.2 库存操作 Mapper 扩展(MoonCakeSeckillGoodsMapper.java)
    • 6.3 服务层设计
      • 6.3.1 秒杀服务接口(SeckillService.java)
      • 6.3.2 秒杀服务实现(SeckillServiceImpl.java)
    • 6.4 库存服务设计
      • 6.4.1 库存服务接口(MoonCakeSeckillGoodsService.java)
      • 6.4.2 库存服务实现(MoonCakeSeckillGoodsServiceImpl.java)
    • 6.5 控制器设计
      • 6.5.1 秒杀控制器(SeckillController.java)
    • 6.6 消息队列设计
      • 6.6.1 消息生产者(SeckillMessageProducer.java)
      • 6.6.2 消息消费者(OrderMessageConsumer.java)
      • 6.6.3 MQ 配置类(RabbitMqConfig.java)
  • 七、高并发处理与性能优化
    • 7.1 多级缓存架构
    • 7.2 流量控制
    • 7.3 防超卖机制
    • 7.4 异步化处理
    • 7.5 数据库优化
  • 八、系统监控与运维
    • 8.1 关键监控指标
    • 8.2 监控实现
    • 8.3 应急预案
  • 九、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档