SpringBoot + Redis 电商秒杀完整方案

一、简介

电商秒杀核心痛点:高并发(十万/百万QPS)、库存超卖、接口限流、防恶意请求、数据一致性。

实现方案:基于 SpringBoot + Redis 实现,核心利用 Redis 原子操作(INCR/DECR/SETNX)、Lua 脚本保证库存原子性,结合 Redis 缓存、消息队列削峰、接口限流,彻底解决秒杀核心问题。

二、方案核心架构(分层设计)

2.1 整体分层(从前端到存储)

  1. 前端层:按钮置灰、倒计时、重复点击拦截、请求防抖(避免用户频繁点击发起请求);
  2. 网关层:接口限流(IP/用户ID限流)、黑名单拦截(恶意请求);
  3. 应用层:Redis 预减库存、Lua 脚本保证原子性、请求校验(用户资格、商品状态);
  4. 缓存层:Redis 存储商品库存、秒杀状态、用户秒杀记录(防重复秒杀);
  5. 消息队列层:RabbitMQ/Kafka 削峰填谷,异步处理订单创建、库存最终扣减;
  6. 数据层:MySQL 存储订单、用户、商品基础数据,通过消息队列异步同步,保证最终一致性。

2.2 核心设计思路

秒杀的核心是"快"和"准":快(避免数据库直接承压)、准(不超卖、不重复秒杀)。

核心逻辑:Redis 预减库存(原子操作)→ 成功则入队 → 异步创建订单 → 数据库扣减库存 → 同步缓存库存;失败则直接返回秒杀失败,全程不操作数据库。

二、环境准备

  • SpringBoot:3.2.x(稳定版,兼容Redis、RabbitMQ);
  • Redis:6.x+(支持Lua脚本、原子操作,建议集群部署提升可用性);
  • RabbitMQ:3.12.x(消息队列,削峰填谷,异步处理订单);
  • MySQL:8.0.x(存储核心业务数据);
  • Redisson:3.20.x(分布式锁备选,应对极端场景,可选);
  • Lombok、Spring Cache(简化开发)。

三、核心实现步骤

第一步:依赖引入(pom.xml)

xml 复制代码
<!-- SpringBoot 核心 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Redis 依赖(Spring Data Redis) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Redis 连接池(性能优化) -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- RabbitMQ 依赖(消息队列) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!-- MySQL 依赖 -->
<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>3.5.5</version>
</dependency>

<!-- Redisson(可选,分布式锁) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.20.1</version>
</dependency>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

第二步:核心配置(application.yml)

配置 Redis、RabbitMQ、MySQL,重点优化 Redis 连接池、消息队列可靠性、接口限流参数。

yaml 复制代码
server:
  port: 8080
  servlet:
    context-path: /seckill

# Spring 核心配置
spring:
  # Redis 配置(秒杀核心缓存)
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 0
    lettuce:
      pool:
        max-active: 1000  # 最大连接数(高并发场景调大)
        max-idle: 100     # 最大空闲连接
        min-idle: 10      # 最小空闲连接
        max-wait: 1000ms  # 连接超时时间
    timeout: 5000ms      # 读取超时时间

  # RabbitMQ 配置(消息队列,削峰填谷)
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual  # 手动ACK,保证消息不丢失
        prefetch: 100             # 每次拉取100条消息,避免消费堆积
    publisher-confirm-type: correlated  # 发布确认,保证消息投递成功
    publisher-returns: true
    template:
      mandatory: true

  # MySQL 配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/seckill_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

# MyBatis-Plus 配置
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.seckill.entity
  configuration:
    map-underscore-to-camel-case: true

# 秒杀自定义配置
seckill:
  redis:
    stock-key-prefix: "seckill:stock:"       # 库存缓存key前缀
    user-seckill-key-prefix: "seckill:user:" # 用户秒杀记录key前缀
  rabbitmq:
    queue-name: "seckill.queue"              # 秒杀消息队列
    exchange-name: "seckill.exchange"        # 秒杀交换机
    routing-key: "seckill.routing.key"       # 路由键
  limit:
    qps: 10000                               # 接口QPS限制(根据服务器配置调整)
    ip-limit: 5                               # 单IP每分钟最大请求数

第三步:数据库设计(核心表)

3张核心表:商品表(seckill_goods)、订单表(seckill_order)、用户表(user),简化设计,聚焦秒杀核心字段。

sql 复制代码
-- 商品表(秒杀商品)
CREATE TABLE seckill_goods (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',
    goods_name VARCHAR(100) NOT NULL COMMENT '商品名称',
    goods_price DECIMAL(10,2) NOT NULL COMMENT '商品原价',
    seckill_price DECIMAL(10,2) NOT NULL COMMENT '秒杀价',
    stock INT NOT NULL COMMENT '秒杀库存',
    seckill_start_time DATETIME NOT NULL COMMENT '秒杀开始时间',
    seckill_end_time DATETIME NOT NULL COMMENT '秒杀结束时间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';

-- 订单表(秒杀订单)
CREATE TABLE seckill_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    goods_id BIGINT NOT NULL COMMENT '商品ID',
    order_price DECIMAL(10,2) NOT NULL COMMENT '订单价格(秒杀价)',
    order_status TINYINT DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    UNIQUE KEY uk_user_goods (user_id, goods_id) COMMENT '唯一索引,防止用户重复下单'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

-- 用户表(简化)
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    password VARCHAR(100) NOT NULL COMMENT '加密密码',
    phone VARCHAR(11) NOT NULL COMMENT '手机号',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

第四步:核心实体类(Entity)

java 复制代码
// 商品实体
@Data
@TableName("seckill_goods")
public class SeckillGoods {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String goodsName;
    private BigDecimal goodsPrice;
    private BigDecimal seckillPrice;
    private Integer stock; // 数据库库存
    private LocalDateTime seckillStartTime;
    private LocalDateTime seckillEndTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

// 订单实体
@Data
@TableName("seckill_order")
public class SeckillOrder {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long goodsId;
    private BigDecimal orderPrice;
    private Integer orderStatus;
    private LocalDateTime createTime;
}

// 秒杀请求DTO(接收前端参数)
@Data
public class SeckillRequestDTO {
    private Long userId;     // 用户ID(登录后获取)
    private Long goodsId;    // 商品ID
}

// 秒杀响应结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SeckillResult {
    private Boolean success; // 秒杀是否成功
    private String message;  // 提示信息
    private Long orderId;    // 订单ID(成功时返回)
}

第五步:Redis 核心操作(预减库存、防重复秒杀)

核心是 Lua 脚本,保证"预减库存+判断重复秒杀"的原子性,避免高并发下的超卖和重复秒杀(Redis 单线程执行 Lua 脚本,无并发问题)。

1. Lua 脚本(秒杀核心逻辑)

脚本功能:判断商品库存是否充足 → 判断用户是否已秒杀 → 预减库存 → 记录用户秒杀记录,全程原子操作。

bash 复制代码
-- seckill.lua
-- 参数:1.商品库存key 2.用户秒杀记录key 3.商品库存阈值(默认1)
local stockKey = KEYS[1]
local userKey = KEYS[2]
local userId = ARGV[1]
local minStock = tonumber(ARGV[2]) or 1

-- 1. 判断库存是否充足(库存 < 1 则秒杀失败)
local stock = redis.call('get', stockKey)
if not stock or tonumber(stock) < minStock then
    return 0 -- 库存不足,返回0
end

-- 2. 判断用户是否已秒杀(避免重复秒杀)
local hasSeckill = redis.call('sismember', userKey, userId)
if hasSeckill == 1 then
    return 2 -- 已秒杀,返回2
end

-- 3. 预减库存(原子操作)
redis.call('decr', stockKey)

-- 4. 记录用户秒杀记录(用集合存储,去重)
redis.call('sadd', userKey, userId)

return 1 -- 秒杀成功,返回1

2. Redis 工具类(加载 Lua 脚本、执行原子操作)

java 复制代码
@Component
@Slf4j
public class RedisSeckillUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 加载Lua脚本(classpath下的seckill.lua)
    private final DefaultRedisScript<Long> seckillScript;

    // 秒杀相关key前缀(从配置文件读取)
    @Value("${seckill.redis.stock-key-prefix}")
    private String stockKeyPrefix;
    @Value("${seckill.redis.user-seckill-key-prefix}")
    private String userSeckillKeyPrefix;

    // 初始化Lua脚本
    public RedisSeckillUtil() {
        seckillScript = new DefaultRedisScript<>();
        seckillScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("seckill.lua")));
        seckillScript.setResultType(Long.class);
    }

    /**
     * 执行秒杀核心逻辑(Lua脚本原子操作)
     * @param goodsId 商品ID
     * @param userId 用户ID
     * @return 0-库存不足,1-秒杀成功,2-已秒杀
     */
    public Long doSeckill(Long goodsId, Long userId) {
        // 构建库存key和用户秒杀记录key
        String stockKey = stockKeyPrefix + goodsId;
        String userKey = userSeckillKeyPrefix + goodsId;

        // 执行Lua脚本,参数:KEYS=[stockKey, userKey],ARGV=[userId, 1]
        Long result = redisTemplate.execute(
                seckillScript,
                Arrays.asList(stockKey, userKey),
                userId.toString(),
                "1"
        );

        log.info("秒杀执行结果:goodsId={}, userId={}, result={}", goodsId, userId, result);
        return result;
    }

    /**
     * 初始化商品库存到Redis(秒杀开始前执行)
     * @param goodsId 商品ID
     * @param stock 库存数量
     */
    public void initStock(Long goodsId, Integer stock) {
        String stockKey = stockKeyPrefix + goodsId;
        redisTemplate.opsForValue().set(stockKey, stock.toString());
        log.info("初始化商品{}库存到Redis,库存数量:{}", goodsId, stock);
    }

    /**
     * 判断用户是否已秒杀
     * @param goodsId 商品ID
     * @param userId 用户ID
     * @return true-已秒杀,false-未秒杀
     */
    public Boolean hasSeckill(Long goodsId, Long userId) {
        String userKey = userSeckillKeyPrefix + goodsId;
        return redisTemplate.opsForSet().isMember(userKey, userId.toString());
    }

    /**
     * 获取当前Redis库存
     * @param goodsId 商品ID
     * @return 库存数量(null表示无此商品)
     */
    public Integer getCurrentStock(Long goodsId) {
        String stockKey = stockKeyPrefix + goodsId;
        String stockStr = redisTemplate.opsForValue().get(stockKey);
        return stockStr == null ? null : Integer.parseInt(stockStr);
    }
}

第六步:接口限流(防高并发压垮系统)

采用 Redis + 自定义注解实现接口限流,支持 IP 限流和 QPS 限流,避免恶意请求和高并发冲击。

1. 限流注解

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SeckillLimit {
    // 限流类型:IP/USER/QPS
    enum LimitType {
        IP, USER, QPS
    }

    // 限流类型(默认QPS)
    LimitType type() default LimitType.QPS;

    // 限流阈值(默认10000 QPS)
    int limit() default 10000;

    // 限流时间窗口(默认1秒)
    int timeWindow() default 1;
}

2. 限流切面(AOP实现)

java 复制代码
@Aspect
@Component
@Slf4j
public class SeckillLimitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 限流key前缀
    private static final String LIMIT_KEY_PREFIX = "seckill:limit:";

    // 切入点:拦截带有@SeckillLimit注解的方法
    @Pointcut("@annotation(com.seckill.annotation.SeckillLimit)")
    public void seckillLimitPointcut() {}

    @Around("seckillLimitPointcut() && @annotation(limit)")
    public Object around(ProceedingJoinPoint joinPoint, SeckillLimit limit) throws Throwable {
        // 1. 获取限流key(根据限流类型生成)
        String limitKey = generateLimitKey(joinPoint, limit);
        // 2. 限流时间窗口(秒)
        int timeWindow = limit.timeWindow();
        // 3. 限流阈值
        int limitCount = limit.limit();

        // 4. Redis原子自增,设置过期时间(时间窗口)
        Long count = redisTemplate.opsForValue().increment(limitKey, 1);
        if (count == 1) {
            // 第一次请求,设置过期时间
            redisTemplate.expire(limitKey, timeWindow, TimeUnit.SECONDS);
        }

        // 5. 判断是否超过限流阈值
        if (count > limitCount) {
            log.warn("触发限流:key={}, 当前请求数={}, 阈值={}", limitKey, count, limitCount);
            // 返回限流提示
            return new SeckillResult(false, "请求过于频繁,请稍后再试", null);
        }

        // 6. 未限流,执行原方法
        return joinPoint.proceed();
    }

    /**
     * 生成限流key
     */
    private String generateLimitKey(ProceedingJoinPoint joinPoint, SeckillLimit limit) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        StringBuilder key = new StringBuilder(LIMIT_KEY_PREFIX);

        // 根据限流类型生成key
        switch (limit.type()) {
            case IP:
                // IP限流:key = 前缀 + IP
                String ip = getClientIp(request);
                key.append("ip:").append(ip);
                break;
            case USER:
                // 用户限流:从请求参数中获取userId(需根据实际场景调整)
                SeckillRequestDTO dto = getSeckillRequestDTO(joinPoint);
                if (dto != null && dto.getUserId() != null) {
                    key.append("user:").append(dto.getUserId());
                } else {
                    // 无用户ID,降级为IP限流
                    key.append("ip:").append(getClientIp(request));
                }
                break;
            case QPS:
                // QPS限流:key = 前缀 + 接口路径
                String requestUri = request.getRequestURI();
                key.append("qps:").append(requestUri);
                break;
        }
        return key.toString();
    }

    /**
     * 获取请求IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 从请求参数中获取SeckillRequestDTO
     */
    private SeckillRequestDTO getSeckillRequestDTO(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof SeckillRequestDTO) {
                return (SeckillRequestDTO) arg;
            }
        }
        return null;
    }
}

第七步:消息队列(削峰填谷,异步处理订单)

秒杀成功后,不直接操作数据库,而是发送消息到 RabbitMQ,异步创建订单、扣减数据库库存,避免高并发直接冲击数据库。

1. 消息队列配置(交换机、队列、绑定)

java 复制代码
@Configuration
public class RabbitMQConfig {

    // 从配置文件读取参数
    @Value("${seckill.rabbitmq.queue-name}")
    private String seckillQueue;
    @Value("${seckill.rabbitmq.exchange-name}")
    private String seckillExchange;
    @Value("${seckill.rabbitmq.routing-key}")
    private String seckillRoutingKey;

    // 1. 声明秒杀队列(持久化)
    @Bean
    public Queue seckillQueue() {
        return QueueBuilder.durable(seckillQueue)
                .withArgument("x-dead-letter-exchange", "seckill.dlx.exchange") // 死信交换机(处理失败消息)
                .withArgument("x-dead-letter-routing-key", "seckill.dlx.routing.key")
                .build();
    }

    // 2. 声明秒杀交换机(Direct类型)
    @Bean
    public DirectExchange seckillExchange() {
        return new DirectExchange(seckillExchange, true, false);
    }

    // 3. 绑定队列和交换机
    @Bean
    public Binding seckillBinding() {
        return BindingBuilder.bind(seckillQueue())
                .to(seckillExchange())
                .with(seckillRoutingKey);
    }

    // 4. 死信队列(处理订单创建失败的消息,用于重试)
    @Bean
    public Queue seckillDlxQueue() {
        return QueueBuilder.durable("seckill.dlx.queue").build();
    }

    // 5. 死信交换机
    @Bean
    public DirectExchange seckillDlxExchange() {
        return new DirectExchange("seckill.dlx.exchange", true, false);
    }

    // 6. 绑定死信队列和死信交换机
    @Bean
    public Binding seckillDlxBinding() {
        return BindingBuilder.bind(seckillDlxQueue())
                .to(seckillDlxExchange())
                .with("seckill.dlx.routing.key");
    }
}

2. 消息发送与消费

java 复制代码
// 1. 消息发送工具类
@Component
@Slf4j
public class RabbitMQSeckillUtil {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Value("${seckill.rabbitmq.exchange-name}")
    private String seckillExchange;
    @Value("${seckill.rabbitmq.routing-key}")
    private String seckillRoutingKey;

    /**
     * 发送秒杀消息(用户ID+商品ID)
     */
    public void sendSeckillMessage(Long userId, Long goodsId) {
        try {
            // 构建消息体(可自定义DTO)
            Map<String, Long> message = new HashMap<>();
            message.put("userId", userId);
            message.put("goodsId", goodsId);

            // 发送消息
            rabbitTemplate.convertAndSend(seckillExchange, seckillRoutingKey, message);
            log.info("秒杀消息发送成功:userId={}, goodsId={}", userId, goodsId);
        } catch (Exception e) {
            log.error("秒杀消息发送失败:userId={}, goodsId={}", userId, goodsId, e);
            // 消息发送失败,可做重试或补偿处理
            throw new RuntimeException("秒杀消息发送失败,请稍后再试");
        }
    }
}

// 2. 消息消费者(异步创建订单、扣减数据库库存)
@Component
@Slf4j
public class SeckillMessageConsumer {

    @Autowired
    private SeckillGoodsService goodsService;
    @Autowired
    private SeckillOrderService orderService;
    @Autowired
    private RedisSeckillUtil redisSeckillUtil;

    /**
     * 消费秒杀消息,创建订单、扣减数据库库存
     */
    @RabbitListener(queues = "${seckill.rabbitmq.queue-name}")
    public void consumeSeckillMessage(Map<String, Long> message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        Long userId = message.get("userId");
        Long goodsId = message.get("goodsId");

        try {
            log.info("开始消费秒杀消息:userId={}, goodsId={}", userId, goodsId);

            // 1. 校验用户和商品(双重校验,避免Redis缓存异常)
            if (userId == null || goodsId == null) {
                log.error("秒杀消息参数异常:userId={}, goodsId={}", userId, goodsId);
                channel.basicAck(deliveryTag, false);
                return;
            }

            // 2. 校验用户是否已秒杀(Redis再次校验)
            if (redisSeckillUtil.hasSeckill(goodsId, userId)) {
                // 3. 扣减数据库库存(乐观锁,防止超卖)
                boolean stockDecrSuccess = goodsService.decrStock(goodsId);
                if (stockDecrSuccess) {
                    // 4. 创建秒杀订单
                    SeckillOrder order = orderService.createSeckillOrder(userId, goodsId);
                    log.info("秒杀订单创建成功:orderId={}, userId={}, goodsId={}", order.getId(), userId, goodsId);
                } else {
                    // 数据库库存不足(Redis预减库存与数据库库存不一致,做补偿)
                    log.error("数据库库存不足:goodsId={}", goodsId);
                    // 恢复Redis库存
                    redisSeckillUtil.initStock(goodsId, goodsService.getById(goodsId).getStock());
                    // 移除用户秒杀记录
                    String userKey = "${seckill.redis.user-seckill-key-prefix}" + goodsId;
                    redisTemplate.opsForSet().remove(userKey, userId.toString());
                }
            } else {
                log.warn("用户已秒杀过:userId={}, goodsId={}", userId, goodsId);
            }

            // 5. 手动ACK,确认消息消费成功
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            log.error("消费秒杀消息失败:userId={}, goodsId={}", userId, goodsId, e);
            // 消费失败,重回队列(最多重试3次,超过则进入死信队列)
            channel.basicNack(deliveryTag, false, true);
        }
    }
}

第八步:业务层实现(商品、订单服务)

核心是数据库库存扣减(乐观锁)、订单创建,保证数据一致性。

java 复制代码
// 商品服务(扣减数据库库存,乐观锁)
@Service
@Slf4j
public class SeckillGoodsServiceImpl implements SeckillGoodsService {

    @Autowired
    private SeckillGoodsMapper goodsMapper;

    /**
     * 扣减数据库库存(乐观锁,防止超卖)
     * @param goodsId 商品ID
     * @return true-扣减成功,false-扣减失败(库存不足)
     */
    @Override
    @Transactional
    public boolean decrStock(Long goodsId) {
        // 乐观锁:where id = #{goodsId} and stock > 0,保证库存不会扣成负数
        int row = goodsMapper.decrStock(goodsId);
        return row > 0;
    }

    /**
     * 根据ID查询商品
     */
    @Override
    public SeckillGoods getById(Long goodsId) {
        return goodsMapper.selectById(goodsId);
    }

    /**
     * 初始化商品库存到Redis(秒杀开始前调用)
     */
    @Override
    public void initStockToRedis(Long goodsId) {
        SeckillGoods goods = getById(goodsId);
        if (goods != null) {
            redisSeckillUtil.initStock(goodsId, goods.getStock());
        }
    }
}

// 订单服务(创建秒杀订单)
@Service
@Slf4j
public class SeckillOrderServiceImpl implements SeckillOrderService {

    @Autowired
    private SeckillOrderMapper orderMapper;
    @Autowired
    private SeckillGoodsService goodsService;

    /**
     * 创建秒杀订单
     * @param userId 用户ID
     * @param goodsId 商品ID
     * @return 订单对象
     */
    @Override
    @Transactional
    public SeckillOrder createSeckillOrder(Long userId, Long goodsId) {
        // 1. 校验用户是否已创建订单(数据库唯一索引再次防重)
        SeckillOrder existOrder = orderMapper.selectByUserIdAndGoodsId(userId, goodsId);
        if (existOrder != null) {
            throw new RuntimeException("您已参与该商品秒杀,不可重复下单");
        }

        // 2. 获取商品信息(秒杀价)
        SeckillGoods goods = goodsService.getById(goodsId);
        if (goods == null) {
            throw new RuntimeException("秒杀商品不存在");
        }

        // 3. 创建订单
        SeckillOrder order = new SeckillOrder();
        order.setUserId(userId);
        order.setGoodsId(goodsId);
        order.setOrderPrice(goods.getSeckillPrice());
        order.setOrderStatus(0); // 0-待支付
        orderMapper.insert(order);

        return order;
    }

    /**
     * 根据用户ID和商品ID查询订单
     */
    @Override
    public SeckillOrder selectByUserIdAndGoodsId(Long userId, Long goodsId) {
        return orderMapper.selectByUserIdAndGoodsId(userId, goodsId);
    }
}

第九步:控制层(秒杀接口)

对外提供秒杀接口,整合限流、Redis预减库存、消息发送逻辑。

java 复制代码
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {

    @Autowired
    private RedisSeckillUtil redisSeckillUtil;
    @Autowired
    private RabbitMQSeckillUtil rabbitMQSeckillUtil;
    @Autowired
    private SeckillGoodsService goodsService;
    @Autowired
    private SeckillOrderService orderService;

    /**
     * 秒杀接口(核心接口)
     * 注解说明:QPS限流10000,IP限流5次/分钟
     */
    @PostMapping("/doSeckill")
    @SeckillLimit(type = SeckillLimit.LimitType.QPS, limit = 10000)
    @SeckillLimit(type = SeckillLimit.LimitType.IP, limit = 5, timeWindow = 60)
    public SeckillResult doSeckill(@RequestBody SeckillRequestDTO dto) {
        Long userId = dto.getUserId();
        Long goodsId = dto.getGoodsId();

        try {
            // 1. 基础校验
            if (userId == null || goodsId == null) {
                return new SeckillResult(false, "参数错误", null);
            }

            // 2. 校验商品状态(是否在秒杀时间内、是否存在)
            SeckillGoods goods = goodsService.getById(goodsId);
            if (goods == null) {
                return new SeckillResult(false, "秒杀商品不存在", null);
            }
            LocalDateTime now = LocalDateTime.now();
            if (now.isBefore(goods.getSeckillStartTime()) || now.isAfter(goods.getSeckillEndTime())) {
                return new SeckillResult(false, "秒杀未开始或已结束", null);
            }

            // 3. Redis预减库存 + 防重复秒杀(Lua脚本原子操作)
            Long result = redisSeckillUtil.doSeckill(goodsId, userId);
            if (result == 0) {
                // 库存不足
                return new SeckillResult(false, "秒杀失败,库存已售罄", null);
            } else if (result == 2) {
                // 已秒杀
                SeckillOrder order = orderService.selectByUserIdAndGoodsId(userId, goodsId);
                return new SeckillResult(false, "您已参与秒杀,请勿重复提交", order != null ? order.getId() : null);
            }

            // 4. 秒杀成功,发送消息到RabbitMQ,异步创建订单
            rabbitMQSeckillUtil.sendSeckillMessage(userId, goodsId);

            // 5. 返回秒杀成功(订单ID后续可通过查询接口获取)
            return new SeckillResult(true, "秒杀成功,请等待订单创建", null);
        } catch (Exception e) {
            log.error("秒杀接口异常:userId={}, goodsId={}", userId, goodsId, e);
            return new SeckillResult(false, "秒杀异常,请稍后再试", null);
        }
    }

    /**
     * 查询秒杀结果(用户查询自己的秒杀订单)
     */
    @GetMapping("/queryResult")
    public SeckillResult queryResult(@RequestParam Long userId, @RequestParam Long goodsId) {
        SeckillOrder order = orderService.selectByUserIdAndGoodsId(userId, goodsId);
        if (order != null) {
            return new SeckillResult(true, "秒杀成功", order.getId());
        } else {
            // 未查询到订单,可能是订单未创建完成或秒杀失败
            Integer stock = redisSeckillUtil.getCurrentStock(goodsId);
            if (stock != null && stock > 0) {
                return new SeckillResult(false, "秒杀处理中,请稍后查询", null);
            } else {
                return new SeckillResult(false, "秒杀失败", null);
            }
        }
    }

    /**
     * 初始化商品库存到Redis(管理员接口,秒杀开始前调用)
     */
    @PostMapping("/initStock")
    public String initStock(@RequestParam Long goodsId) {
        goodsService.initStockToRedis(goodsId);
        return "库存初始化成功";
    }
}

四、核心优化方案

4.1 库存预减优化(Redis + Lua)

核心优化点:用 Lua 脚本保证"预减库存+防重复秒杀"原子性,避免高并发下的超卖;Redis 集群部署,提升可用性和吞吐量。

4.2 接口限流优化

  • 前端限流:按钮置灰、倒计时、重复点击拦截,减少无效请求;
  • 网关限流:Nginx 配置 IP 限流,提前拦截部分恶意请求;
  • 应用限流:AOP + Redis 实现 QPS/IP/用户限流,避免压垮应用。

4.3 消息队列削峰

秒杀成功的请求先入队,消费者异步处理订单创建和库存扣减,将瞬时高并发转化为异步处理,避免数据库直接承压。

4.4 数据库优化

  • 乐观锁扣减库存:避免行锁,提升并发处理能力;
  • 索引优化:订单表添加 (user_id, goods_id) 唯一索引,商品表添加 goods_id 主键索引;
  • 分库分表:秒杀商品和订单表按商品ID/用户ID分库分表,应对海量数据。

4.5 缓存优化

  • 商品信息缓存:将秒杀商品信息缓存到 Redis,避免频繁查询数据库;
  • 库存缓存预热:秒杀开始前,将商品库存初始化到 Redis;
  • 本地缓存:用 Caffeine 本地缓存,缓存热点商品信息,减少 Redis 访问压力。

4.6 防恶意请求

  • 黑名单拦截:将恶意请求IP、恶意用户加入黑名单,禁止访问;
  • 验证码:秒杀前添加图形验证码/短信验证码,防止脚本刷单;
  • 用户资格校验:校验用户是否登录、是否有秒杀资格(如会员等级)。

五、生产环境部署与防护

5.1 部署架构

采用微服务部署,核心组件集群化:

  • SpringBoot 应用:多实例部署,负载均衡(Nginx/网关);
  • Redis:主从+哨兵/Redis Cluster,保证高可用;
  • RabbitMQ:集群部署,开启镜像队列,保证消息不丢失;
  • MySQL:主从复制,读写分离,秒杀订单写入主库,查询读从库。

5.2 监控与告警

  • Redis 监控:监控库存缓存、请求量、命中率,库存不足时告警;
  • 消息队列监控:监控消息堆积量、消费速率,堆积过多时告警;
  • 应用监控:用 SpringBoot Actuator + Prometheus + Grafana 监控接口QPS、响应时间、异常率;
  • 数据库监控:监控库存扣减成功率、订单创建速率,异常时告警。

5.3 异常处理与补偿

  • 消息消费失败:进入死信队列,定时重试,重试3次失败则人工介入;
  • Redis 与数据库库存不一致:定时任务校验,发现不一致则同步库存;
  • 应用宕机:重启后,从 Redis 读取用户秒杀记录,重新发送消息到队列,保证订单创建。

六、常见问题与解决方案

6.1 库存超卖

原因:高并发下,Redis 预减库存与数据库扣减不同步;无原子操作。

解决方案:用 Lua 脚本保证原子性;数据库乐观锁扣减;定时同步库存。

6.2 重复秒杀

原因:用户重复点击;消息重试导致重复创建订单。

解决方案:Redis 集合记录用户秒杀;订单表唯一索引;消费时双重校验。

6.3 接口响应慢

原因:高并发下 Redis 访问压力大;数据库操作耗时。

解决方案:Redis 集群;本地缓存;异步处理订单;数据库优化。

6.4 消息堆积

原因:消费速率低于生产速率;消费者宕机。

解决方案:增加消费者实例;优化消费逻辑;设置消息超时时间。

6.5 Redis 宕机

原因:Redis 单节点故障;集群切换不及时。

解决方案:Redis 集群部署;开启哨兵;降级为数据库直接扣减(应急方案)。

相关推荐
会编程的吕洞宾8 小时前
Spring_Boot_3_3_的___Transactional__
java·后端·spring
阿聪谈架构8 小时前
第11章:结构化输出与数据提取 —— 让 AI 直接返回你想要的数据格式
人工智能·后端
神奇小汤圆8 小时前
Java面试八股文+场景题+答案,100万字精华版,全网仅此一份
后端
那个失眠的夜8 小时前
SpringBoot
java·开发语言·spring boot·spring·mvc·mybatis
数据仓库搬砖人8 小时前
XGBoost 调参指南
后端
会编程的土豆8 小时前
Go 连接 Redis 代码详细解析
开发语言·redis·golang
学以智用8 小时前
.NET Core 仓储模式(Repository Pattern)完整教程
后端·.net
叫我少年8 小时前
Quartz.NET 调度框架:从入门到封装实战
后端
Java编程爱好者8 小时前
MySQL事务实战:MySQL实例 · 隔离级别 · InnoDB实现机制
后端