秒杀系统设计与实现

仿京东秒杀系统设计与实现

前言

秒杀系统是电商平台中最具挑战性的业务场景之一。每逢618、双11等大促活动,京东、淘宝等平台都会面临数百万甚至上千万的并发请求。本文将从架构设计、核心技术、代码实现等方面,详细讲解如何设计一个高并发、高可用的秒杀系统。

一、秒杀系统的业务特点

1.1 秒杀业务的核心挑战

diff 复制代码
+------------------+     +------------------+     +------------------+
|    瞬时高并发     |     |    库存超卖风险   |     |   数据一致性     |
|  100万+ QPS     |     |   超卖=亏损      |     |  缓存与DB同步    |
+------------------+     +------------------+     +------------------+
         |                       |                       |
         v                       v                       v
+------------------+     +------------------+     +------------------+
|    服务雪崩       |     |    恶意请求       |     |   用户体验       |
|   系统崩溃风险    |     |   刷单/机器人     |     |   响应时间要求    |
+------------------+     +------------------+     +------------------+

1.2 秒杀系统的核心指标

指标 要求 说明
QPS 100万+ 峰值每秒请求数
响应时间 <100ms 用户可接受的等待时间
可用性 99.99% 系统稳定性要求
库存准确性 100% 绝对不能超卖

二、系统架构设计

2.1 整体架构图

lua 复制代码
                              +------------------+
                              |     用户请求      |
                              +--------+---------+
                                       |
                              +--------v---------+
                              |      CDN         |
                              |   静态资源分发    |
                              +--------+---------+
                                       |
                              +--------v---------+
                              |    Nginx集群     |
                              |   负载均衡/限流   |
                              +--------+---------+
                                       |
         +-----------------------------+-----------------------------+
         |                             |                             |
+--------v---------+         +--------v---------+         +--------v---------+
|   Gateway网关    |         |   Gateway网关    |         |   Gateway网关    |
|  认证/限流/路由   |         |  认证/限流/路由   |         |  认证/限流/路由   |
+--------+---------+         +--------+---------+         +--------+---------+
         |                             |                             |
         +-----------------------------+-----------------------------+
                                       |
         +-----------------------------+-----------------------------+
         |                             |                             |
+--------v---------+         +--------v---------+         +--------v---------+
|   秒杀服务集群    |         |   秒杀服务集群    |         |   秒杀服务集群    |
|   业务逻辑处理    |         |   业务逻辑处理    |         |   业务逻辑处理    |
+--------+---------+         +--------+---------+         +--------+---------+
         |                             |                             |
         +-----------------------------+-----------------------------+
                                       |
                    +------------------+------------------+
                    |                  |                  |
           +--------v-------+  +-------v--------+  +------v-------+
           |   Redis集群    |  |  RocketMQ集群  |  |  MySQL集群   |
           |   库存/限流    |  |   异步削峰     |  |   订单存储   |
           +----------------+  +----------------+  +--------------+

2.2 核心组件说明

  1. CDN层:静态页面、图片等资源缓存
  2. Nginx层:负载均衡、流量控制、静态化
  3. Gateway层:统一入口、认证鉴权、限流熔断
  4. 服务层:秒杀核心业务逻辑
  5. 缓存层:Redis集群,库存预热、分布式锁
  6. 消息队列:RocketMQ异步处理,削峰填谷
  7. 数据库层:MySQL主从集群,最终数据持久化

三、数据库设计

3.1 核心表结构

sql 复制代码
-- 秒杀商品表
CREATE TABLE `seckill_goods` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
    `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
    `goods_name` VARCHAR(128) NOT NULL COMMENT '商品名称',
    `goods_img` VARCHAR(256) DEFAULT NULL COMMENT '商品图片',
    `original_price` DECIMAL(10,2) NOT NULL COMMENT '原价',
    `seckill_price` DECIMAL(10,2) NOT NULL COMMENT '秒杀价',
    `stock_count` INT(11) NOT NULL COMMENT '库存数量',
    `start_time` DATETIME NOT NULL COMMENT '秒杀开始时间',
    `end_time` DATETIME NOT NULL COMMENT '秒杀结束时间',
    `status` TINYINT(1) DEFAULT 0 COMMENT '状态:0-未开始,1-进行中,2-已结束',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_goods_id` (`goods_id`),
    KEY `idx_start_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';

-- 秒杀订单表
CREATE TABLE `seckill_order` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
    `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
    `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
    `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
    `seckill_goods_id` BIGINT(20) NOT NULL COMMENT '秒杀商品ID',
    `goods_name` VARCHAR(128) NOT NULL COMMENT '商品名称',
    `goods_price` DECIMAL(10,2) NOT NULL COMMENT '商品价格',
    `order_status` TINYINT(1) DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_goods` (`user_id`, `seckill_goods_id`),
    UNIQUE KEY `uk_order_no` (`order_no`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

-- 库存流水表(用于库存对账)
CREATE TABLE `stock_log` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `seckill_goods_id` BIGINT(20) NOT NULL,
    `order_no` VARCHAR(32) NOT NULL,
    `stock_change` INT(11) NOT NULL COMMENT '库存变化:负数为扣减',
    `type` TINYINT(1) NOT NULL COMMENT '类型:1-扣减,2-回滚',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_seckill_goods_id` (`seckill_goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';

四、核心技术实现

4.1 项目结构

bash 复制代码
seckill-system/
├── seckill-common/                 # 公共模块
│   ├── src/main/java/
│   │   └── com/seckill/common/
│   │       ├── constant/           # 常量定义
│   │       ├── enums/              # 枚举类
│   │       ├── exception/          # 异常定义
│   │       ├── result/             # 统一返回
│   │       └── utils/              # 工具类
├── seckill-gateway/                # 网关服务
├── seckill-service/                # 秒杀服务
│   ├── src/main/java/
│   │   └── com/seckill/service/
│   │       ├── controller/         # 控制器
│   │       ├── service/            # 业务逻辑
│   │       ├── mapper/             # 数据访问
│   │       ├── entity/             # 实体类
│   │       ├── dto/                # 数据传输对象
│   │       ├── config/             # 配置类
│   │       └── mq/                 # 消息队列
└── seckill-admin/                  # 管理后台

4.2 实体类定义

java 复制代码
package com.seckill.service.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 秒杀商品实体
 */
@Data
@TableName("seckill_goods")
public class SeckillGoods {

    @TableId(type = IdType.AUTO)
    private Long id;

    private Long goodsId;

    private String goodsName;

    private String goodsImg;

    private BigDecimal originalPrice;

    private BigDecimal seckillPrice;

    private Integer stockCount;

    private LocalDateTime startTime;

    private LocalDateTime endTime;

    private Integer status;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
java 复制代码
package com.seckill.service.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 秒杀订单实体
 */
@Data
@TableName("seckill_order")
public class SeckillOrder {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String orderNo;

    private Long userId;

    private Long goodsId;

    private Long seckillGoodsId;

    private String goodsName;

    private BigDecimal goodsPrice;

    private Integer orderStatus;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    private LocalDateTime payTime;
}

4.3 统一响应结果

java 复制代码
package com.seckill.common.result;

import lombok.Data;
import java.io.Serializable;

/**
 * 统一响应结果
 */
@Data
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer code;
    private String message;
    private T data;

    private Result() {}

    public static <T> Result<T> success() {
        return success(null);
    }

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }

    public static <T> Result<T> error(Integer code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static <T> Result<T> error(ResultCode resultCode) {
        return error(resultCode.getCode(), resultCode.getMessage());
    }
}
java 复制代码
package com.seckill.common.result;

import lombok.Getter;

/**
 * 响应状态码枚举
 */
@Getter
public enum ResultCode {

    SUCCESS(200, "操作成功"),
    ERROR(500, "系统错误"),

    // 秒杀相关
    SECKILL_NOT_START(1001, "秒杀活动未开始"),
    SECKILL_END(1002, "秒杀活动已结束"),
    SECKILL_STOCK_EMPTY(1003, "商品已售罄"),
    SECKILL_REPEAT(1004, "请勿重复秒杀"),
    SECKILL_LIMIT(1005, "访问过于频繁,请稍后重试"),
    SECKILL_QUEUE(1006, "排队中,请稍后查询结果"),

    // 用户相关
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_NOT_EXIST(2002, "用户不存在");

    private final Integer code;
    private final String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

五、限流与防刷策略

5.1 限流架构

sql 复制代码
                    +----------------------+
                    |      用户请求         |
                    +----------+-----------+
                               |
                    +----------v-----------+
                    |    Nginx限流         |
                    |  limit_req_zone      |
                    +----------+-----------+
                               |
                    +----------v-----------+
                    |   Gateway限流        |
                    |  Sentinel/自定义     |
                    +----------+-----------+
                               |
                    +----------v-----------+
                    |   接口级别限流        |
                    |  @RateLimiter        |
                    +----------+-----------+
                               |
                    +----------v-----------+
                    |   用户级别限流        |
                    |  Redis + Lua         |
                    +----------------------+

5.2 Redis + Lua 实现分布式限流

java 复制代码
package com.seckill.service.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

/**
 * Redis配置类
 */
@Configuration
public class RedisConfig {

    /**
     * 限流Lua脚本
     */
    @Bean
    public DefaultRedisScript<Long> rateLimitScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(
            new ClassPathResource("lua/rate_limit.lua")));
        script.setResultType(Long.class);
        return script;
    }

    /**
     * 库存扣减Lua脚本
     */
    @Bean
    public DefaultRedisScript<Long> stockDeductScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(
            new ClassPathResource("lua/stock_deduct.lua")));
        script.setResultType(Long.class);
        return script;
    }
}

rate_limit.lua - 滑动窗口限流脚本:

lua 复制代码
-- 限流Lua脚本:滑动窗口算法
-- KEYS[1]: 限流的key
-- ARGV[1]: 限流阈值
-- ARGV[2]: 时间窗口(秒)
-- ARGV[3]: 当前时间戳

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 移除时间窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)

if count < limit then
    -- 添加当前请求
    redis.call('ZADD', key, now, now .. '-' .. math.random())
    redis.call('EXPIRE', key, window)
    return 1  -- 允许通过
else
    return 0  -- 限流
end

5.3 限流服务实现

java 复制代码
package com.seckill.service.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;

/**
 * 限流服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RateLimitService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisScript<Long> rateLimitScript;

    private static final String RATE_LIMIT_PREFIX = "seckill:limit:";

    /**
     * 检查是否被限流
     * @param userId 用户ID
     * @param goodsId 商品ID
     * @param limit 限流阈值
     * @param windowSeconds 时间窗口(秒)
     * @return true-允许通过, false-被限流
     */
    public boolean tryAcquire(Long userId, Long goodsId, int limit, int windowSeconds) {
        String key = RATE_LIMIT_PREFIX + goodsId + ":" + userId;
        long now = System.currentTimeMillis();

        Long result = redisTemplate.execute(
            rateLimitScript,
            Collections.singletonList(key),
            limit, windowSeconds, now
        );

        return result != null && result == 1;
    }

    /**
     * IP维度限流
     */
    public boolean tryAcquireByIp(String ip, int limit, int windowSeconds) {
        String key = RATE_LIMIT_PREFIX + "ip:" + ip;
        long now = System.currentTimeMillis();

        Long result = redisTemplate.execute(
            rateLimitScript,
            Collections.singletonList(key),
            limit, windowSeconds, now
        );

        return result != null && result == 1;
    }
}

5.4 自定义限流注解

java 复制代码
package com.seckill.service.annotation;

import java.lang.annotation.*;

/**
 * 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /**
     * 限流key前缀
     */
    String prefix() default "";

    /**
     * 限流阈值
     */
    int limit() default 10;

    /**
     * 时间窗口(秒)
     */
    int window() default 1;

    /**
     * 限流维度:USER/IP/GLOBAL
     */
    LimitType type() default LimitType.USER;

    enum LimitType {
        USER,   // 用户维度
        IP,     // IP维度
        GLOBAL  // 全局维度
    }
}
java 复制代码
package com.seckill.service.aspect;

import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.annotation.RateLimit;
import com.seckill.service.service.RateLimitService;
import com.seckill.service.utils.IpUtils;
import com.seckill.service.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 限流切面
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {

    private final RateLimitService rateLimitService;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
        boolean allowed = false;

        switch (rateLimit.type()) {
            case USER:
                Long userId = UserContext.getCurrentUserId();
                allowed = rateLimitService.tryAcquire(
                    userId, 0L, rateLimit.limit(), rateLimit.window());
                break;
            case IP:
                HttpServletRequest request = getRequest();
                String ip = IpUtils.getClientIp(request);
                allowed = rateLimitService.tryAcquireByIp(
                    ip, rateLimit.limit(), rateLimit.window());
                break;
            case GLOBAL:
                // 全局限流实现
                break;
        }

        if (!allowed) {
            log.warn("请求被限流: type={}", rateLimit.type());
            throw new SeckillException(ResultCode.SECKILL_LIMIT);
        }

        return point.proceed();
    }

    private HttpServletRequest getRequest() {
        ServletRequestAttributes attrs = (ServletRequestAttributes)
            RequestContextHolder.getRequestAttributes();
        return attrs != null ? attrs.getRequest() : null;
    }
}

六、库存扣减方案

6.1 库存扣减方案对比

diff 复制代码
+------------------+------------------+------------------+------------------+
|      方案        |       优点       |       缺点       |     适用场景     |
+------------------+------------------+------------------+------------------+
|   数据库扣减     |  数据强一致性    |  性能差,并发低   |   低并发场景     |
+------------------+------------------+------------------+------------------+
|  Redis预扣减    |  性能高          |  需处理一致性     |   高并发场景     |
+------------------+------------------+------------------+------------------+
|  Redis+MQ异步   |  性能最高        |  最终一致性       |   超高并发       |
+------------------+------------------+------------------+------------------+

6.2 Redis库存预热

java 复制代码
package com.seckill.service.service;

import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.mapper.SeckillGoodsMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 库存预热服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class StockWarmUpService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final SeckillGoodsMapper seckillGoodsMapper;

    private static final String STOCK_KEY_PREFIX = "seckill:stock:";
    private static final String GOODS_INFO_PREFIX = "seckill:goods:";

    /**
     * 系统启动时预热库存
     */
    @PostConstruct
    public void warmUpOnStartup() {
        log.info("系统启动,开始预热秒杀库存...");
        warmUpStock();
    }

    /**
     * 定时任务:每5分钟检查并预热即将开始的秒杀活动
     */
    @Scheduled(cron = "0 */5 * * * ?")
    public void scheduledWarmUp() {
        log.info("定时任务:检查秒杀活动库存预热...");
        warmUpStock();
    }

    /**
     * 预热库存到Redis
     */
    public void warmUpStock() {
        // 查询即将开始或正在进行的秒杀活动(未来30分钟内开始的)
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime future = now.plusMinutes(30);

        List<SeckillGoods> goodsList = seckillGoodsMapper.selectActiveGoods(now, future);

        for (SeckillGoods goods : goodsList) {
            String stockKey = STOCK_KEY_PREFIX + goods.getId();
            String infoKey = GOODS_INFO_PREFIX + goods.getId();

            // 检查是否已预热
            if (Boolean.TRUE.equals(redisTemplate.hasKey(stockKey))) {
                log.debug("商品[{}]库存已预热,跳过", goods.getId());
                continue;
            }

            // 预热库存
            redisTemplate.opsForValue().set(stockKey, goods.getStockCount());

            // 预热商品信息
            redisTemplate.opsForValue().set(infoKey, goods);

            // 设置过期时间(活动结束后1小时过期)
            long expireSeconds = java.time.Duration.between(
                now, goods.getEndTime().plusHours(1)).getSeconds();
            redisTemplate.expire(stockKey, expireSeconds, TimeUnit.SECONDS);
            redisTemplate.expire(infoKey, expireSeconds, TimeUnit.SECONDS);

            log.info("商品[{}]库存预热完成,库存数量: {}", goods.getId(), goods.getStockCount());
        }
    }

    /**
     * 手动预热指定商品
     */
    public void warmUpByGoodsId(Long seckillGoodsId) {
        SeckillGoods goods = seckillGoodsMapper.selectById(seckillGoodsId);
        if (goods == null) {
            log.warn("商品[{}]不存在", seckillGoodsId);
            return;
        }

        String stockKey = STOCK_KEY_PREFIX + goods.getId();
        redisTemplate.opsForValue().set(stockKey, goods.getStockCount());
        log.info("商品[{}]手动预热完成,库存: {}", seckillGoodsId, goods.getStockCount());
    }
}

6.3 Redis + Lua 原子扣减库存

stock_deduct.lua - 库存扣减脚本:

lua 复制代码
-- 库存扣减Lua脚本
-- KEYS[1]: 库存key
-- KEYS[2]: 已购买用户集合key
-- ARGV[1]: 用户ID
-- ARGV[2]: 扣减数量(默认1)

local stockKey = KEYS[1]
local boughtKey = KEYS[2]
local userId = ARGV[1]
local deductNum = tonumber(ARGV[2]) or 1

-- 检查用户是否已购买
if redis.call('SISMEMBER', boughtKey, userId) == 1 then
    return -1  -- 重复购买
end

-- 获取当前库存
local stock = tonumber(redis.call('GET', stockKey) or 0)

if stock < deductNum then
    return -2  -- 库存不足
end

-- 扣减库存
redis.call('DECRBY', stockKey, deductNum)

-- 记录用户已购买
redis.call('SADD', boughtKey, userId)

-- 返回剩余库存
return stock - deductNum

6.4 库存服务实现

java 复制代码
package com.seckill.service.service;

import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/**
 * 库存服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class StockService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisScript<Long> stockDeductScript;

    private static final String STOCK_KEY_PREFIX = "seckill:stock:";
    private static final String BOUGHT_KEY_PREFIX = "seckill:bought:";

    /**
     * 预扣减库存(Redis)
     * @return 剩余库存,-1表示重复购买,-2表示库存不足
     */
    public long preDeductStock(Long seckillGoodsId, Long userId) {
        String stockKey = STOCK_KEY_PREFIX + seckillGoodsId;
        String boughtKey = BOUGHT_KEY_PREFIX + seckillGoodsId;

        Long result = redisTemplate.execute(
            stockDeductScript,
            Arrays.asList(stockKey, boughtKey),
            userId.toString(), 1
        );

        if (result == null) {
            throw new SeckillException(ResultCode.ERROR);
        }

        return result;
    }

    /**
     * 回滚库存(订单取消或支付超时)
     */
    public void rollbackStock(Long seckillGoodsId, Long userId) {
        String stockKey = STOCK_KEY_PREFIX + seckillGoodsId;
        String boughtKey = BOUGHT_KEY_PREFIX + seckillGoodsId;

        // 库存+1
        redisTemplate.opsForValue().increment(stockKey);
        // 移除已购买记录
        redisTemplate.opsForSet().remove(boughtKey, userId.toString());

        log.info("库存回滚成功,seckillGoodsId={}, userId={}", seckillGoodsId, userId);
    }

    /**
     * 获取剩余库存
     */
    public Integer getStock(Long seckillGoodsId) {
        String stockKey = STOCK_KEY_PREFIX + seckillGoodsId;
        Object stock = redisTemplate.opsForValue().get(stockKey);
        return stock != null ? Integer.parseInt(stock.toString()) : 0;
    }

    /**
     * 检查用户是否已购买
     */
    public boolean hasUserBought(Long seckillGoodsId, Long userId) {
        String boughtKey = BOUGHT_KEY_PREFIX + seckillGoodsId;
        return Boolean.TRUE.equals(
            redisTemplate.opsForSet().isMember(boughtKey, userId.toString()));
    }
}

七、异步下单与消息队列

7.1 异步下单流程

lua 复制代码
+------------+     +------------+     +------------+     +------------+
|  用户请求   | --> |  库存预检   | --> |  Redis扣减  | --> |  发送MQ    |
+------------+     +------------+     +------------+     +------------+
                                                               |
                   +-------------------------------------------+
                   |
                   v
+------------+     +------------+     +------------+     +------------+
|  返回排队   | <-- |  生成排队号  | <-- |   入队成功   | <-- | Producer  |
+------------+     +------------+     +------------+     +------------+


+------------+     +------------+     +------------+     +------------+
| Consumer   | --> |  创建订单   | --> |  扣减DB库存  | --> |  写入结果  |
+------------+     +------------+     +------------+     +------------+

7.2 RocketMQ消息生产者

java 复制代码
package com.seckill.service.mq;

import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.SeckillMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 秒杀消息生产者
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillMessageProducer {

    private final RocketMQTemplate rocketMQTemplate;

    private static final String SECKILL_TOPIC = "seckill-order-topic";

    /**
     * 发送秒杀消息(异步)
     */
    public void sendSeckillMessage(SeckillMessage message) {
        String jsonMessage = JSON.toJSONString(message);

        rocketMQTemplate.asyncSend(SECKILL_TOPIC,
            MessageBuilder.withPayload(jsonMessage).build(),
            new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    log.info("秒杀消息发送成功: msgId={}, userId={}, goodsId={}",
                        sendResult.getMsgId(), message.getUserId(), message.getSeckillGoodsId());
                }

                @Override
                public void onException(Throwable e) {
                    log.error("秒杀消息发送失败: userId={}, goodsId={}",
                        message.getUserId(), message.getSeckillGoodsId(), e);
                    // 发送失败,回滚Redis库存
                    // stockService.rollbackStock(message.getSeckillGoodsId(), message.getUserId());
                }
            });
    }

    /**
     * 发送秒杀消息(同步,用于需要确保消息发送成功的场景)
     */
    public SendResult sendSeckillMessageSync(SeckillMessage message) {
        String jsonMessage = JSON.toJSONString(message);
        return rocketMQTemplate.syncSend(SECKILL_TOPIC,
            MessageBuilder.withPayload(jsonMessage).build());
    }
}

7.3 消息DTO

java 复制代码
package com.seckill.service.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 秒杀消息DTO
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 秒杀商品ID
     */
    private Long seckillGoodsId;

    /**
     * 商品ID
     */
    private Long goodsId;

    /**
     * 排队号
     */
    private String queueNo;

    /**
     * 请求时间戳
     */
    private Long timestamp;
}

7.4 RocketMQ消息消费者

java 复制代码
package com.seckill.service.mq;

import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
 * 秒杀消息消费者
 */
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
    topic = "seckill-order-topic",
    consumerGroup = "seckill-order-consumer-group",
    consumeMode = ConsumeMode.ORDERLY  // 顺序消费
)
public class SeckillMessageConsumer implements RocketMQListener<String> {

    private final OrderService orderService;

    @Override
    public void onMessage(String message) {
        log.info("收到秒杀消息: {}", message);

        try {
            SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);

            // 创建订单
            orderService.createOrder(seckillMessage);

            log.info("订单创建成功: userId={}, goodsId={}",
                seckillMessage.getUserId(), seckillMessage.getSeckillGoodsId());

        } catch (Exception e) {
            log.error("处理秒杀消息失败: {}", message, e);
            // 这里可以发送到死信队列,或者记录到数据库待后续处理
            throw e;  // 抛出异常,消息将被重试
        }
    }
}

7.5 订单服务

java 复制代码
package com.seckill.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.entity.SeckillOrder;
import com.seckill.service.mapper.SeckillGoodsMapper;
import com.seckill.service.mapper.SeckillOrderMapper;
import com.seckill.service.utils.OrderNoGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

/**
 * 订单服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private final SeckillOrderMapper orderMapper;
    private final SeckillGoodsMapper goodsMapper;
    private final StockService stockService;
    private final RedisTemplate<String, Object> redisTemplate;

    private static final String ORDER_RESULT_PREFIX = "seckill:result:";

    /**
     * 创建订单(异步消费时调用)
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(SeckillMessage message) {
        Long userId = message.getUserId();
        Long seckillGoodsId = message.getSeckillGoodsId();

        // 1. 再次检查是否已下单(幂等性)
        SeckillOrder existOrder = orderMapper.selectOne(
            new LambdaQueryWrapper<SeckillOrder>()
                .eq(SeckillOrder::getUserId, userId)
                .eq(SeckillOrder::getSeckillGoodsId, seckillGoodsId)
        );
        if (existOrder != null) {
            log.warn("用户已下单,跳过: userId={}, goodsId={}", userId, seckillGoodsId);
            // 写入成功结果
            writeResult(userId, seckillGoodsId, existOrder.getOrderNo(), true, "订单已存在");
            return;
        }

        // 2. 查询商品信息
        SeckillGoods goods = goodsMapper.selectById(seckillGoodsId);
        if (goods == null) {
            log.error("商品不存在: seckillGoodsId={}", seckillGoodsId);
            writeResult(userId, seckillGoodsId, null, false, "商品不存在");
            return;
        }

        // 3. 数据库扣减库存(乐观锁)
        int affected = goodsMapper.deductStock(seckillGoodsId, 1);
        if (affected == 0) {
            log.warn("数据库库存不足: seckillGoodsId={}", seckillGoodsId);
            // 回滚Redis库存
            stockService.rollbackStock(seckillGoodsId, userId);
            writeResult(userId, seckillGoodsId, null, false, "库存不足");
            return;
        }

        // 4. 创建订单
        String orderNo = OrderNoGenerator.generate();
        SeckillOrder order = new SeckillOrder();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setGoodsId(goods.getGoodsId());
        order.setSeckillGoodsId(seckillGoodsId);
        order.setGoodsName(goods.getGoodsName());
        order.setGoodsPrice(goods.getSeckillPrice());
        order.setOrderStatus(0);  // 待支付

        orderMapper.insert(order);

        // 5. 写入秒杀结果
        writeResult(userId, seckillGoodsId, orderNo, true, "秒杀成功");

        log.info("订单创建成功: orderNo={}, userId={}, goodsId={}",
            orderNo, userId, seckillGoodsId);
    }

    /**
     * 写入秒杀结果到Redis
     */
    private void writeResult(Long userId, Long seckillGoodsId, String orderNo,
                             boolean success, String message) {
        String key = ORDER_RESULT_PREFIX + userId + ":" + seckillGoodsId;
        SeckillResult result = new SeckillResult(success, orderNo, message);
        redisTemplate.opsForValue().set(key, result, 30, TimeUnit.MINUTES);
    }

    /**
     * 查询秒杀结果
     */
    public SeckillResult getSeckillResult(Long userId, Long seckillGoodsId) {
        String key = ORDER_RESULT_PREFIX + userId + ":" + seckillGoodsId;
        return (SeckillResult) redisTemplate.opsForValue().get(key);
    }

    /**
     * 秒杀结果内部类
     */
    @lombok.Data
    @lombok.AllArgsConstructor
    @lombok.NoArgsConstructor
    public static class SeckillResult {
        private boolean success;
        private String orderNo;
        private String message;
    }
}

7.6 Mapper层

java 复制代码
package com.seckill.service.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.seckill.service.entity.SeckillGoods;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.time.LocalDateTime;
import java.util.List;

@Mapper
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {

    /**
     * 乐观锁扣减库存
     */
    @Update("UPDATE seckill_goods SET stock_count = stock_count - #{count} " +
            "WHERE id = #{id} AND stock_count >= #{count}")
    int deductStock(@Param("id") Long id, @Param("count") Integer count);

    /**
     * 查询活跃的秒杀活动
     */
    @Select("SELECT * FROM seckill_goods " +
            "WHERE start_time <= #{future} AND end_time >= #{now}")
    List<SeckillGoods> selectActiveGoods(@Param("now") LocalDateTime now,
                                          @Param("future") LocalDateTime future);
}

八、秒杀接口实现

8.1 秒杀Controller

java 复制代码
package com.seckill.service.controller;

import com.seckill.common.result.Result;
import com.seckill.common.result.ResultCode;
import com.seckill.service.annotation.RateLimit;
import com.seckill.service.dto.SeckillRequest;
import com.seckill.service.service.OrderService;
import com.seckill.service.service.SeckillService;
import com.seckill.service.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * 秒杀接口
 */
@Slf4j
@RestController
@RequestMapping("/seckill")
@RequiredArgsConstructor
public class SeckillController {

    private final SeckillService seckillService;
    private final OrderService orderService;

    /**
     * 执行秒杀
     */
    @PostMapping("/do")
    @RateLimit(limit = 5, window = 1, type = RateLimit.LimitType.USER)
    public Result<String> doSeckill(@Valid @RequestBody SeckillRequest request) {
        Long userId = UserContext.getCurrentUserId();
        Long seckillGoodsId = request.getSeckillGoodsId();

        log.info("秒杀请求: userId={}, goodsId={}", userId, seckillGoodsId);

        // 执行秒杀
        String queueNo = seckillService.doSeckill(userId, seckillGoodsId);

        return Result.success(queueNo);
    }

    /**
     * 查询秒杀结果
     */
    @GetMapping("/result/{seckillGoodsId}")
    public Result<OrderService.SeckillResult> getSeckillResult(
            @PathVariable Long seckillGoodsId) {
        Long userId = UserContext.getCurrentUserId();

        OrderService.SeckillResult result = orderService.getSeckillResult(userId, seckillGoodsId);

        if (result == null) {
            // 还在排队
            return Result.error(ResultCode.SECKILL_QUEUE);
        }

        return Result.success(result);
    }

    /**
     * 获取秒杀商品列表
     */
    @GetMapping("/goods/list")
    public Result<?> getGoodsList() {
        return Result.success(seckillService.getSeckillGoodsList());
    }

    /**
     * 获取秒杀商品详情
     */
    @GetMapping("/goods/{id}")
    public Result<?> getGoodsDetail(@PathVariable Long id) {
        return Result.success(seckillService.getSeckillGoodsDetail(id));
    }
}

8.2 秒杀核心服务

java 复制代码
package com.seckill.service.service;

import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.mapper.SeckillGoodsMapper;
import com.seckill.service.mq.SeckillMessageProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

/**
 * 秒杀核心服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillService {

    private final StockService stockService;
    private final SeckillMessageProducer messageProducer;
    private final SeckillGoodsMapper seckillGoodsMapper;
    private final RedisTemplate<String, Object> redisTemplate;

    private static final String GOODS_INFO_PREFIX = "seckill:goods:";

    /**
     * 执行秒杀
     */
    public String doSeckill(Long userId, Long seckillGoodsId) {
        // 1. 检查秒杀活动状态
        SeckillGoods goods = getSeckillGoods(seckillGoodsId);
        checkSeckillTime(goods);

        // 2. 检查用户是否已购买(内存标记优化,减少Redis访问)
        if (stockService.hasUserBought(seckillGoodsId, userId)) {
            throw new SeckillException(ResultCode.SECKILL_REPEAT);
        }

        // 3. Redis预扣减库存
        long stockResult = stockService.preDeductStock(seckillGoodsId, userId);

        if (stockResult == -1) {
            throw new SeckillException(ResultCode.SECKILL_REPEAT);
        }
        if (stockResult == -2) {
            // 标记商品已售罄(内存标记)
            markGoodsSoldOut(seckillGoodsId);
            throw new SeckillException(ResultCode.SECKILL_STOCK_EMPTY);
        }

        // 4. 生成排队号
        String queueNo = generateQueueNo(userId, seckillGoodsId);

        // 5. 发送MQ消息,异步创建订单
        SeckillMessage message = SeckillMessage.builder()
                .userId(userId)
                .seckillGoodsId(seckillGoodsId)
                .goodsId(goods.getGoodsId())
                .queueNo(queueNo)
                .timestamp(System.currentTimeMillis())
                .build();

        messageProducer.sendSeckillMessage(message);

        log.info("秒杀请求已入队: userId={}, goodsId={}, queueNo={}",
            userId, seckillGoodsId, queueNo);

        return queueNo;
    }

    /**
     * 获取秒杀商品(优先从缓存获取)
     */
    private SeckillGoods getSeckillGoods(Long seckillGoodsId) {
        String key = GOODS_INFO_PREFIX + seckillGoodsId;
        SeckillGoods goods = (SeckillGoods) redisTemplate.opsForValue().get(key);

        if (goods == null) {
            goods = seckillGoodsMapper.selectById(seckillGoodsId);
            if (goods == null) {
                throw new SeckillException(ResultCode.ERROR);
            }
        }

        return goods;
    }

    /**
     * 检查秒杀时间
     */
    private void checkSeckillTime(SeckillGoods goods) {
        LocalDateTime now = LocalDateTime.now();

        if (now.isBefore(goods.getStartTime())) {
            throw new SeckillException(ResultCode.SECKILL_NOT_START);
        }

        if (now.isAfter(goods.getEndTime())) {
            throw new SeckillException(ResultCode.SECKILL_END);
        }
    }

    /**
     * 标记商品售罄
     */
    private void markGoodsSoldOut(Long seckillGoodsId) {
        // 可以使用本地缓存或Redis标记
        String key = "seckill:soldout:" + seckillGoodsId;
        redisTemplate.opsForValue().set(key, true);
    }

    /**
     * 生成排队号
     */
    private String generateQueueNo(Long userId, Long seckillGoodsId) {
        return String.format("%d-%d-%s",
            seckillGoodsId, userId, UUID.randomUUID().toString().substring(0, 8));
    }

    /**
     * 获取秒杀商品列表
     */
    public List<SeckillGoods> getSeckillGoodsList() {
        LocalDateTime now = LocalDateTime.now();
        return seckillGoodsMapper.selectActiveGoods(now, now.plusDays(1));
    }

    /**
     * 获取秒杀商品详情
     */
    public SeckillGoods getSeckillGoodsDetail(Long id) {
        return getSeckillGoods(id);
    }
}

九、分布式锁实现

9.1 Redisson分布式锁

java 复制代码
package com.seckill.service.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 分布式锁服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DistributedLockService {

    private final RedissonClient redissonClient;

    private static final String LOCK_PREFIX = "seckill:lock:";

    /**
     * 尝试获取锁并执行操作
     */
    public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime,
                                  TimeUnit unit, Supplier<T> supplier) {
        RLock lock = redissonClient.getLock(LOCK_PREFIX + lockKey);

        try {
            boolean acquired = lock.tryLock(waitTime, leaseTime, unit);
            if (!acquired) {
                log.warn("获取锁失败: lockKey={}", lockKey);
                return null;
            }

            log.debug("获取锁成功: lockKey={}", lockKey);
            return supplier.get();

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取锁被中断: lockKey={}", lockKey, e);
            return null;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.debug("释放锁: lockKey={}", lockKey);
            }
        }
    }

    /**
     * 使用看门狗机制的锁(自动续期)
     */
    public <T> T executeWithWatchdog(String lockKey, Supplier<T> supplier) {
        RLock lock = redissonClient.getLock(LOCK_PREFIX + lockKey);

        try {
            // 不指定leaseTime,使用看门狗自动续期
            lock.lock();
            return supplier.get();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

9.2 Redisson配置

java 复制代码
package com.seckill.service.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redisson配置
 */
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password:}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        // 单机模式
        config.useSingleServer()
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password.isEmpty() ? null : password)
                .setDatabase(0)
                .setConnectionMinimumIdleSize(10)
                .setConnectionPoolSize(64);

        // 集群模式(生产环境推荐)
        // config.useClusterServers()
        //     .addNodeAddress("redis://node1:6379", "redis://node2:6379")
        //     .setPassword(password);

        return Redisson.create(config);
    }
}

十、高可用与容错

10.1 Sentinel熔断降级配置

java 复制代码
package com.seckill.service.config;

import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

/**
 * Sentinel配置
 */
@Configuration
public class SentinelConfig {

    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }

    @PostConstruct
    public void initRules() {
        initFlowRules();
        initDegradeRules();
    }

    /**
     * 初始化限流规则
     */
    private void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();

        // 秒杀接口限流:每秒最多处理10000个请求
        FlowRule seckillRule = new FlowRule();
        seckillRule.setResource("doSeckill");
        seckillRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        seckillRule.setCount(10000);
        seckillRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        seckillRule.setWarmUpPeriodSec(10);  // 预热时间10秒
        rules.add(seckillRule);

        // 查询接口限流
        FlowRule queryRule = new FlowRule();
        queryRule.setResource("getSeckillResult");
        queryRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        queryRule.setCount(50000);
        rules.add(queryRule);

        FlowRuleManager.loadRules(rules);
    }

    /**
     * 初始化熔断规则
     */
    private void initDegradeRules() {
        List<DegradeRule> rules = new ArrayList<>();

        // 慢调用比例熔断
        DegradeRule slowRule = new DegradeRule();
        slowRule.setResource("doSeckill");
        slowRule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
        slowRule.setCount(100);  // 阈值RT(毫秒)
        slowRule.setTimeWindow(10);  // 熔断时长(秒)
        slowRule.setSlowRatioThreshold(0.5);  // 慢调用比例阈值
        slowRule.setMinRequestAmount(10);  // 最小请求数
        slowRule.setStatIntervalMs(10000);  // 统计时长(毫秒)
        rules.add(slowRule);

        // 异常比例熔断
        DegradeRule exceptionRule = new DegradeRule();
        exceptionRule.setResource("doSeckill");
        exceptionRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
        exceptionRule.setCount(0.5);  // 异常比例阈值
        exceptionRule.setTimeWindow(30);  // 熔断时长(秒)
        exceptionRule.setMinRequestAmount(10);
        rules.add(exceptionRule);

        DegradeRuleManager.loadRules(rules);
    }
}

10.2 熔断降级处理

java 复制代码
package com.seckill.service.service;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.seckill.common.result.Result;
import com.seckill.common.result.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * 带熔断降级的秒杀服务
 */
@Slf4j
@Service
public class SeckillServiceWithSentinel {

    @SentinelResource(
        value = "doSeckill",
        blockHandler = "doSeckillBlockHandler",
        fallback = "doSeckillFallback"
    )
    public Result<String> doSeckillWithSentinel(Long userId, Long seckillGoodsId) {
        // 秒杀逻辑
        return Result.success("排队中");
    }

    /**
     * 限流/熔断处理
     */
    public Result<String> doSeckillBlockHandler(Long userId, Long seckillGoodsId,
                                                 BlockException ex) {
        log.warn("秒杀接口被限流/熔断: userId={}, goodsId={}", userId, seckillGoodsId);
        return Result.error(ResultCode.SECKILL_LIMIT);
    }

    /**
     * 降级处理
     */
    public Result<String> doSeckillFallback(Long userId, Long seckillGoodsId,
                                             Throwable ex) {
        log.error("秒杀接口异常降级: userId={}, goodsId={}", userId, seckillGoodsId, ex);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

十一、订单超时处理

11.1 延迟队列方案

lua 复制代码
                    +-------------------+
                    |    创建订单        |
                    +--------+----------+
                             |
                    +--------v----------+
                    |  发送延迟消息      |
                    |  (30分钟后触发)    |
                    +--------+----------+
                             |
                             | 30分钟后
                             |
                    +--------v----------+
                    |  消费延迟消息      |
                    +--------+----------+
                             |
              +--------------+--------------+
              |                             |
    +---------v---------+         +---------v---------+
    |   订单已支付       |         |    订单未支付      |
    |   忽略消息         |         |    取消订单        |
    +-------------------+         |    回滚库存        |
                                  +-------------------+

11.2 RocketMQ延迟消息实现

java 复制代码
package com.seckill.service.mq;

import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.OrderTimeoutMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 订单超时消息生产者
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderTimeoutProducer {

    private final RocketMQTemplate rocketMQTemplate;

    private static final String TOPIC = "order-timeout-topic";

    // RocketMQ延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
    private static final int DELAY_LEVEL_30_MIN = 16;  // 30分钟

    /**
     * 发送订单超时检查消息
     */
    public void sendOrderTimeoutMessage(String orderNo, Long userId, Long seckillGoodsId) {
        OrderTimeoutMessage message = new OrderTimeoutMessage();
        message.setOrderNo(orderNo);
        message.setUserId(userId);
        message.setSeckillGoodsId(seckillGoodsId);
        message.setCreateTime(System.currentTimeMillis());

        String jsonMessage = JSON.toJSONString(message);

        Message<String> msg = MessageBuilder
                .withPayload(jsonMessage)
                .build();

        // 发送延迟消息
        rocketMQTemplate.syncSend(TOPIC, msg, 3000, DELAY_LEVEL_30_MIN);

        log.info("订单超时检查消息已发送: orderNo={}", orderNo);
    }
}
java 复制代码
package com.seckill.service.mq;

import com.alibaba.fastjson.JSON;
import com.seckill.service.dto.OrderTimeoutMessage;
import com.seckill.service.service.OrderCancelService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
 * 订单超时消息消费者
 */
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
    topic = "order-timeout-topic",
    consumerGroup = "order-timeout-consumer-group"
)
public class OrderTimeoutConsumer implements RocketMQListener<String> {

    private final OrderCancelService orderCancelService;

    @Override
    public void onMessage(String message) {
        log.info("收到订单超时消息: {}", message);

        try {
            OrderTimeoutMessage timeoutMessage = JSON.parseObject(message,
                OrderTimeoutMessage.class);

            // 处理订单超时
            orderCancelService.handleOrderTimeout(timeoutMessage.getOrderNo());

        } catch (Exception e) {
            log.error("处理订单超时消息失败: {}", message, e);
        }
    }
}

11.3 订单取消服务

java 复制代码
package com.seckill.service.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.seckill.service.entity.SeckillOrder;
import com.seckill.service.mapper.SeckillGoodsMapper;
import com.seckill.service.mapper.SeckillOrderMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 订单取消服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderCancelService {

    private final SeckillOrderMapper orderMapper;
    private final SeckillGoodsMapper goodsMapper;
    private final StockService stockService;

    /**
     * 处理订单超时
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleOrderTimeout(String orderNo) {
        // 1. 查询订单
        SeckillOrder order = orderMapper.selectOne(
            new LambdaQueryWrapper<SeckillOrder>()
                .eq(SeckillOrder::getOrderNo, orderNo)
        );

        if (order == null) {
            log.warn("订单不存在: orderNo={}", orderNo);
            return;
        }

        // 2. 检查订单状态
        if (order.getOrderStatus() != 0) {
            // 非待支付状态,不处理
            log.info("订单状态非待支付,跳过: orderNo={}, status={}",
                orderNo, order.getOrderStatus());
            return;
        }

        // 3. 更新订单状态为已取消
        int affected = orderMapper.update(null,
            new LambdaUpdateWrapper<SeckillOrder>()
                .eq(SeckillOrder::getOrderNo, orderNo)
                .eq(SeckillOrder::getOrderStatus, 0)  // 乐观锁
                .set(SeckillOrder::getOrderStatus, 2)  // 已取消
        );

        if (affected == 0) {
            log.info("订单状态已变更,跳过: orderNo={}", orderNo);
            return;
        }

        // 4. 恢复数据库库存
        goodsMapper.deductStock(order.getSeckillGoodsId(), -1);  // 负数表示增加

        // 5. 恢复Redis库存
        stockService.rollbackStock(order.getSeckillGoodsId(), order.getUserId());

        log.info("订单超时取消成功: orderNo={}", orderNo);
    }
}

十二、安全防护

12.1 接口防重放

java 复制代码
package com.seckill.service.interceptor;

import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

/**
 * 防重放攻击拦截器
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ReplayAttackInterceptor implements HandlerInterceptor {

    private final RedisTemplate<String, Object> redisTemplate;

    private static final String NONCE_PREFIX = "seckill:nonce:";
    private static final long NONCE_EXPIRE_SECONDS = 300;  // 5分钟

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                            Object handler) {
        // 获取请求头中的签名信息
        String timestamp = request.getHeader("X-Timestamp");
        String nonce = request.getHeader("X-Nonce");
        String signature = request.getHeader("X-Signature");

        // 1. 验证时间戳(防止重放攻击)
        if (!validateTimestamp(timestamp)) {
            throw new SeckillException(ResultCode.ERROR);
        }

        // 2. 验证nonce(防止重复请求)
        if (!validateNonce(nonce)) {
            throw new SeckillException(ResultCode.ERROR);
        }

        // 3. 验证签名
        if (!validateSignature(request, timestamp, nonce, signature)) {
            throw new SeckillException(ResultCode.ERROR);
        }

        return true;
    }

    private boolean validateTimestamp(String timestamp) {
        if (timestamp == null) {
            return false;
        }

        try {
            long ts = Long.parseLong(timestamp);
            long now = System.currentTimeMillis();
            // 请求时间与服务器时间差不超过5分钟
            return Math.abs(now - ts) < NONCE_EXPIRE_SECONDS * 1000;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    private boolean validateNonce(String nonce) {
        if (nonce == null || nonce.isEmpty()) {
            return false;
        }

        String key = NONCE_PREFIX + nonce;
        Boolean success = redisTemplate.opsForValue().setIfAbsent(
            key, "1", NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    private boolean validateSignature(HttpServletRequest request, String timestamp,
                                       String nonce, String signature) {
        // 获取密钥(可以从用户Token中获取)
        String secretKey = "user_secret_key";

        // 构建签名字符串
        String signStr = String.format("%s&%s&%s&%s",
            request.getMethod(),
            request.getRequestURI(),
            timestamp,
            nonce
        );

        // 计算签名
        String expectedSignature = DigestUtils.md5DigestAsHex(
            (signStr + "&" + secretKey).getBytes());

        return expectedSignature.equals(signature);
    }
}

12.2 验证码防护

java 复制代码
package com.seckill.service.service;

import com.google.code.kaptcha.Producer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 验证码服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CaptchaService {

    private final Producer captchaProducer;
    private final RedisTemplate<String, Object> redisTemplate;

    private static final String CAPTCHA_PREFIX = "seckill:captcha:";
    private static final long CAPTCHA_EXPIRE_SECONDS = 300;  // 5分钟

    /**
     * 生成验证码
     */
    public CaptchaResult generateCaptcha() {
        // 生成验证码文本
        String captchaText = captchaProducer.createText();

        // 生成验证码图片
        BufferedImage image = captchaProducer.createImage(captchaText);

        // 转换为Base64
        String base64Image = imageToBase64(image);

        // 生成Token
        String captchaToken = UUID.randomUUID().toString();

        // 存储到Redis
        String key = CAPTCHA_PREFIX + captchaToken;
        redisTemplate.opsForValue().set(key, captchaText,
            CAPTCHA_EXPIRE_SECONDS, TimeUnit.SECONDS);

        return new CaptchaResult(captchaToken, "data:image/png;base64," + base64Image);
    }

    /**
     * 验证验证码
     */
    public boolean verifyCaptcha(String captchaToken, String captchaCode) {
        if (captchaToken == null || captchaCode == null) {
            return false;
        }

        String key = CAPTCHA_PREFIX + captchaToken;
        String storedCode = (String) redisTemplate.opsForValue().get(key);

        // 验证后删除
        redisTemplate.delete(key);

        return captchaCode.equalsIgnoreCase(storedCode);
    }

    private String imageToBase64(BufferedImage image) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            ImageIO.write(image, "png", baos);
            return Base64.getEncoder().encodeToString(baos.toByteArray());
        } catch (IOException e) {
            log.error("图片转Base64失败", e);
            return "";
        }
    }

    @lombok.Data
    @lombok.AllArgsConstructor
    public static class CaptchaResult {
        private String token;
        private String image;
    }
}

十三、性能优化

13.1 本地缓存优化

java 复制代码
package com.seckill.service.cache;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.seckill.service.entity.SeckillGoods;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 本地缓存(Caffeine)
 */
@Component
public class LocalCache {

    /**
     * 商品信息缓存
     */
    private final Cache<Long, SeckillGoods> goodsCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build();

    /**
     * 商品售罄标记缓存
     */
    private final Cache<Long, Boolean> soldOutCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .build();

    /**
     * 用户已购买标记缓存
     */
    private final Cache<String, Boolean> userBoughtCache = Caffeine.newBuilder()
            .maximumSize(100000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();

    // 商品信息缓存操作
    public void putGoods(Long goodsId, SeckillGoods goods) {
        goodsCache.put(goodsId, goods);
    }

    public SeckillGoods getGoods(Long goodsId) {
        return goodsCache.getIfPresent(goodsId);
    }

    // 售罄标记操作
    public void markSoldOut(Long goodsId) {
        soldOutCache.put(goodsId, true);
    }

    public boolean isSoldOut(Long goodsId) {
        return Boolean.TRUE.equals(soldOutCache.getIfPresent(goodsId));
    }

    // 用户购买标记操作
    public void markUserBought(Long userId, Long goodsId) {
        userBoughtCache.put(userId + ":" + goodsId, true);
    }

    public boolean hasUserBought(Long userId, Long goodsId) {
        return Boolean.TRUE.equals(userBoughtCache.getIfPresent(userId + ":" + goodsId));
    }
}

13.2 优化后的秒杀服务

java 复制代码
package com.seckill.service.service;

import com.seckill.common.exception.SeckillException;
import com.seckill.common.result.ResultCode;
import com.seckill.service.cache.LocalCache;
import com.seckill.service.dto.SeckillMessage;
import com.seckill.service.entity.SeckillGoods;
import com.seckill.service.mq.SeckillMessageProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 优化后的秒杀服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OptimizedSeckillService {

    private final LocalCache localCache;
    private final StockService stockService;
    private final SeckillMessageProducer messageProducer;

    /**
     * 优化后的秒杀流程
     */
    public String doSeckill(Long userId, Long seckillGoodsId) {

        // 第1层:本地缓存检查售罄标记(内存级别,纳秒级)
        if (localCache.isSoldOut(seckillGoodsId)) {
            throw new SeckillException(ResultCode.SECKILL_STOCK_EMPTY);
        }

        // 第2层:本地缓存检查用户是否已购买
        if (localCache.hasUserBought(userId, seckillGoodsId)) {
            throw new SeckillException(ResultCode.SECKILL_REPEAT);
        }

        // 第3层:获取商品信息(本地缓存 + Redis二级缓存)
        SeckillGoods goods = getGoodsWithCache(seckillGoodsId);

        // 第4层:检查秒杀时间
        checkSeckillTime(goods);

        // 第5层:Redis预扣减库存(原子操作)
        long stockResult = stockService.preDeductStock(seckillGoodsId, userId);

        if (stockResult == -1) {
            localCache.markUserBought(userId, seckillGoodsId);
            throw new SeckillException(ResultCode.SECKILL_REPEAT);
        }

        if (stockResult == -2) {
            localCache.markSoldOut(seckillGoodsId);
            throw new SeckillException(ResultCode.SECKILL_STOCK_EMPTY);
        }

        // 标记用户已购买
        localCache.markUserBought(userId, seckillGoodsId);

        // 如果库存即将售罄,标记售罄
        if (stockResult <= 0) {
            localCache.markSoldOut(seckillGoodsId);
        }

        // 第6层:发送MQ消息,异步创建订单
        String queueNo = generateQueueNo(userId, seckillGoodsId);

        SeckillMessage message = SeckillMessage.builder()
                .userId(userId)
                .seckillGoodsId(seckillGoodsId)
                .goodsId(goods.getGoodsId())
                .queueNo(queueNo)
                .timestamp(System.currentTimeMillis())
                .build();

        messageProducer.sendSeckillMessage(message);

        return queueNo;
    }

    private SeckillGoods getGoodsWithCache(Long seckillGoodsId) {
        // 先查本地缓存
        SeckillGoods goods = localCache.getGoods(seckillGoodsId);
        if (goods != null) {
            return goods;
        }

        // 再查Redis/DB
        goods = stockService.getGoodsFromRedis(seckillGoodsId);
        if (goods != null) {
            localCache.putGoods(seckillGoodsId, goods);
        }

        return goods;
    }

    private void checkSeckillTime(SeckillGoods goods) {
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(goods.getStartTime())) {
            throw new SeckillException(ResultCode.SECKILL_NOT_START);
        }
        if (now.isAfter(goods.getEndTime())) {
            throw new SeckillException(ResultCode.SECKILL_END);
        }
    }

    private String generateQueueNo(Long userId, Long seckillGoodsId) {
        return String.format("%d-%d-%s",
            seckillGoodsId, userId, UUID.randomUUID().toString().substring(0, 8));
    }
}

13.3 请求过滤优化

lua 复制代码
                    用户请求(100万QPS)
                           |
              +------------+------------+
              |        第1层过滤        |
              |    本地缓存售罄检查     |
              |    过滤掉90%请求        |
              +------------+------------+
                           |
                     10万QPS
                           |
              +------------+------------+
              |        第2层过滤        |
              |    用户重复购买检查     |
              |    过滤掉50%请求        |
              +------------+------------+
                           |
                      5万QPS
                           |
              +------------+------------+
              |        第3层过滤        |
              |    Redis库存预扣减      |
              |    按实际库存过滤       |
              +------------+------------+
                           |
                     1000 QPS
                           |
              +------------+------------+
              |        第4层           |
              |    消息队列异步处理     |
              |    最终创建订单         |
              +------------+------------+

十四、监控与告警

14.1 Prometheus监控指标

java 复制代码
package com.seckill.service.monitor;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * 秒杀监控指标
 */
@Component
@RequiredArgsConstructor
public class SeckillMetrics {

    private final MeterRegistry meterRegistry;

    private Counter seckillRequestCounter;
    private Counter seckillSuccessCounter;
    private Counter seckillFailCounter;
    private Counter stockEmptyCounter;
    private Counter repeatBuyCounter;
    private Timer seckillTimer;

    @PostConstruct
    public void init() {
        // 秒杀请求总数
        seckillRequestCounter = Counter.builder("seckill_request_total")
                .description("秒杀请求总数")
                .register(meterRegistry);

        // 秒杀成功数
        seckillSuccessCounter = Counter.builder("seckill_success_total")
                .description("秒杀成功总数")
                .register(meterRegistry);

        // 秒杀失败数
        seckillFailCounter = Counter.builder("seckill_fail_total")
                .description("秒杀失败总数")
                .register(meterRegistry);

        // 库存不足次数
        stockEmptyCounter = Counter.builder("seckill_stock_empty_total")
                .description("库存不足次数")
                .register(meterRegistry);

        // 重复购买次数
        repeatBuyCounter = Counter.builder("seckill_repeat_buy_total")
                .description("重复购买次数")
                .register(meterRegistry);

        // 秒杀耗时
        seckillTimer = Timer.builder("seckill_duration")
                .description("秒杀接口耗时")
                .register(meterRegistry);
    }

    public void recordRequest() {
        seckillRequestCounter.increment();
    }

    public void recordSuccess() {
        seckillSuccessCounter.increment();
    }

    public void recordFail() {
        seckillFailCounter.increment();
    }

    public void recordStockEmpty() {
        stockEmptyCounter.increment();
    }

    public void recordRepeatBuy() {
        repeatBuyCounter.increment();
    }

    public void recordDuration(long durationMs) {
        seckillTimer.record(durationMs, TimeUnit.MILLISECONDS);
    }
}

14.2 日志规范

java 复制代码
package com.seckill.service.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * 秒杀日志切面
 */
@Slf4j
@Aspect
@Component
public class SeckillLogAspect {

    @Around("execution(* com.seckill.service.service.SeckillService.doSeckill(..))")
    public Object logSeckill(ProceedingJoinPoint point) throws Throwable {
        Object[] args = point.getArgs();
        Long userId = (Long) args[0];
        Long goodsId = (Long) args[1];

        long startTime = System.currentTimeMillis();
        String traceId = generateTraceId();

        log.info("[秒杀] traceId={}, userId={}, goodsId={}, action=START",
            traceId, userId, goodsId);

        try {
            Object result = point.proceed();
            long duration = System.currentTimeMillis() - startTime;

            log.info("[秒杀] traceId={}, userId={}, goodsId={}, action=SUCCESS, " +
                     "duration={}ms, result={}",
                traceId, userId, goodsId, duration, result);

            return result;

        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;

            log.warn("[秒杀] traceId={}, userId={}, goodsId={}, action=FAIL, " +
                     "duration={}ms, error={}",
                traceId, userId, goodsId, duration, e.getMessage());

            throw e;
        }
    }

    private String generateTraceId() {
        return String.valueOf(System.nanoTime());
    }
}

十五、配置文件

15.1 application.yml

yaml 复制代码
server:
  port: 8080
  tomcat:
    threads:
      max: 500
      min-spare: 50
    accept-count: 1000

spring:
  application:
    name: seckill-service

  # 数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    hikari:
      minimum-idle: 10
      maximum-pool-size: 50
      idle-timeout: 30000
      max-lifetime: 1800000
      connection-timeout: 30000

  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    lettuce:
      pool:
        max-active: 100
        max-idle: 50
        min-idle: 10
        max-wait: 3000ms

# RocketMQ配置
rocketmq:
  name-server: localhost:9876
  producer:
    group: seckill-producer-group
    send-message-timeout: 3000
    retry-times-when-send-failed: 2
    retry-times-when-send-async-failed: 2

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 日志配置
logging:
  level:
    com.seckill: debug
    org.springframework: info
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"

# 秒杀配置
seckill:
  # 库存预热提前时间(分钟)
  warmup-advance-minutes: 30
  # 订单超时时间(分钟)
  order-timeout-minutes: 30
  # 用户限流阈值
  user-rate-limit: 5
  # IP限流阈值
  ip-rate-limit: 10

十六、总结

16.1 核心技术点回顾

sql 复制代码
+------------------+------------------+----------------------------------+
|      层级        |      技术        |             作用                 |
+------------------+------------------+----------------------------------+
|     接入层       |    Nginx         |   负载均衡、静态资源、限流        |
+------------------+------------------+----------------------------------+
|     网关层       |    Gateway       |   统一入口、认证、路由            |
+------------------+------------------+----------------------------------+
|     限流层       |  Sentinel/Lua    |   多维度限流、熔断降级            |
+------------------+------------------+----------------------------------+
|     缓存层       |   Redis/Local    |   库存预热、分布式锁              |
+------------------+------------------+----------------------------------+
|     消息层       |    RocketMQ      |   异步削峰、延迟队列              |
+------------------+------------------+----------------------------------+
|     服务层       |   Spring Boot    |   业务逻辑处理                    |
+------------------+------------------+----------------------------------+
|     数据层       |   MySQL          |   数据持久化、乐观锁              |
+------------------+------------------+----------------------------------+

16.2 关键设计原则

  1. 流量漏斗:层层过滤,减少到达数据库的请求
  2. 异步解耦:消息队列削峰填谷,提高系统吞吐量
  3. 缓存为王:多级缓存策略,减少数据库压力
  4. 原子操作:Lua脚本保证库存扣减的原子性
  5. 降级兜底:熔断降级保护系统稳定性
  6. 幂等设计:确保重复请求不会产生副作用

16.3 生产环境建议

  1. 容量规划:根据预估QPS规划服务器数量
  2. 压力测试:上线前进行全链路压测
  3. 灰度发布:逐步放开流量,观察系统表现
  4. 监控告警:完善的监控体系,及时发现问题
  5. 应急预案:准备降级方案,应对突发情况

相关推荐
小坏讲微服务2 小时前
Spring Cloud Alibaba 整合 Scala 教程完整使用
java·开发语言·分布式·spring cloud·sentinel·scala·后端开发
老鼠只爱大米2 小时前
Java设计模式之外观模式(Facade)详解
java·设计模式·外观模式·facade·java设计模式
vx_dmxq2112 小时前
【微信小程序学习交流平台】(免费领源码+演示录像)|可做计算机毕设Java、Python、PHP、小程序APP、C#、爬虫大数据、单片机、文案
java·spring boot·python·mysql·微信小程序·小程序·idea
9号达人2 小时前
优惠系统演进:从"实时结算"到"所见即所得",前端传参真的鸡肋吗?
java·后端·面试
AAA简单玩转程序设计2 小时前
Java进阶小妙招:ArrayList和LinkedList的"相爱相杀"
java
lkbhua莱克瓦242 小时前
集合进阶8——Stream流
java·开发语言·笔记·github·stream流·学习方法·集合
20岁30年经验的码农3 小时前
Java Elasticsearch 实战指南
java·开发语言·elasticsearch
okseekw3 小时前
Java 中的注释与关键字的初步学习
java
luv_sw3 小时前
JavaSE-面向对象-构造器
java