欢迎光临
我们一直在努力

Java高并发秒杀系统设计与实现(完整版)

前言

秒杀是电商系统中最典型的高并发场景,特点是:

  • 瞬时流量大:大量用户在同一时间抢购少量商品

  • 库存少:通常只有几百到几千件

  • 时间短:秒杀活动通常持续几秒到几分钟

本文将从零开始设计并实现一个完整的秒杀系统,涵盖:

  • 秒杀系统架构设计

  • 数据库表设计

  • Redis缓存预热与库存扣减

  • 消息队列异步下单

  • 分布式锁防重复下单

  • 限流防刷策略

  • 完整的Java代码实现

技术栈:Spring Boot + Redis + RabbitMQ + MySQL + Lua脚本


一、秒杀系统核心挑战

1.1 高并发带来的问题

问题描述解决方案
超卖 库存扣减不一致 Redis原子操作 + 数据库乐观锁
重复下单 同一用户多次下单 分布式锁 + 唯一索引
数据库压力 大量请求直接打到数据库 Redis预扣库存 + 消息队列削峰
恶意刷单 脚本抢购 限流 + 验证码 + 设备指纹
数据一致性 缓存与数据库不一致 最终一致性方案

1.2 秒杀系统架构设计

                  ┌─────────────────────────────────────────────────────────┐
                  │                     用户请求                           │
                  └─────────────────────────────────────────────────────────┘
                                            │
                                            ▼
                  ┌─────────────────────────────────────────────────────────┐
                  │                   Nginx负载均衡                         │
                  │             (限流、静态资源缓存)                         │
                  └─────────────────────────────────────────────────────────┘
                                            │
                                            ▼
                  ┌─────────────────────────────────────────────────────────┐
                  │                 网关层 (Spring Cloud Gateway)           │
                  │           (全局限流、用户身份验证、请求过滤)               │
                  └─────────────────────────────────────────────────────────┘
                                            │
                                            ▼
                  ┌─────────────────────────────────────────────────────────┐
                  │                   秒杀服务集群                           │
                  │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐     │
                  │ │ 秒杀服务1   │ │ 秒杀服务2   │ │ 秒杀服务3   │     │
                  │ └─────────────┘ └─────────────┘ └─────────────┘     │
                  └─────────────────────────────────────────────────────────┘
                        │                   │                   │
                        ▼                   ▼                   ▼
                  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
                  │   Redis   │ │ RabbitMQ   │ │   MySQL     │
                  │ (库存缓存) │ │ (异步下单)   │ │ (持久化)   │
                  └─────────────┘ └─────────────┘ └─────────────┘

1.3 核心设计原则

  • 尽量将请求拦截在上游:限流、验证码、库存校验

  • 异步处理:下单请求放入消息队列,异步处理

  • 缓存优先:库存预热到Redis,减少数据库访问

  • 最终一致性:允许短暂不一致,保证最终数据正确


  • 二、数据库表设计

    2.1 秒杀活动表

    CREATE TABLE `seckill_activity` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动ID',
    `activity_name` varchar(100) NOT NULL COMMENT '活动名称',
    `product_id` bigint NOT NULL COMMENT '商品ID',
    `product_name` varchar(200) NOT NULL COMMENT '商品名称',
    `product_image` varchar(500) DEFAULT NULL COMMENT '商品图片',
    `original_price` decimal(10,2) NOT NULL COMMENT '原价',
    `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价',
    `total_stock` int NOT NULL COMMENT '总库存',
    `available_stock` int NOT NULL COMMENT '可用库存',
    `start_time` datetime NOT NULL COMMENT '秒杀开始时间',
    `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-未开始,1-进行中,2-已结束',
    `limit_per_user` int NOT NULL DEFAULT 1 COMMENT '每人限购数量',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    KEY `idx_product_id` (`product_id`),
    KEY `idx_start_time` (`start_time`),
    KEY `idx_status` (`status`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀活动表';

    2.2 秒杀订单表

    CREATE TABLE `seckill_order` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
    `order_no` varchar(64) NOT NULL COMMENT '订单编号',
    `activity_id` bigint NOT NULL COMMENT '活动ID',
    `user_id` bigint NOT NULL COMMENT '用户ID',
    `product_id` bigint NOT NULL COMMENT '商品ID',
    `product_name` varchar(200) NOT NULL COMMENT '商品名称',
    `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价',
    `quantity` int NOT NULL DEFAULT 1 COMMENT '购买数量',
    `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待支付,1-已支付,2-已取消,3-已退款',
    `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_no` (`order_no`),
    UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`) COMMENT '同一活动同一用户只能下一单',
    KEY `idx_user_id` (`user_id`),
    KEY `idx_activity_id` (`activity_id`),
    KEY `idx_status` (`status`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

    2.3 秒杀商品表

    CREATE TABLE `seckill_product` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
    `product_name` varchar(200) NOT NULL COMMENT '商品名称',
    `product_image` varchar(500) DEFAULT NULL COMMENT '商品图片',
    `product_detail` text COMMENT '商品详情',
    `original_price` decimal(10,2) NOT NULL COMMENT '原价',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';


    三、Redis缓存设计

    3.1 Redis Key设计

    /**
    * Redis Key 常量定义
    */
    public class RedisKeyConstant {

       /**
        * 秒杀库存Key
        * 格式:seckill:stock:{activityId}
        * 类型:String (Integer)
        */
       public static final String SECKILL_STOCK = "seckill:stock:%s";

       /**
        * 秒杀活动信息Key
        * 格式:seckill:activity:{activityId}
        * 类型:Hash
        */
       public static final String SECKILL_ACTIVITY = "seckill:activity:%s";

       /**
        * 用户已购数量Key
        * 格式:seckill:user:buy:{activityId}:{userId}
        * 类型:String (Integer)
        */
       public static final String SECKILL_USER_BUY = "seckill:user:buy:%s:%s";

       /**
        * 秒杀订单锁Key
        * 格式:seckill:order:lock:{activityId}:{userId}
        * 类型:String
        */
       public static final String SECKILL_ORDER_LOCK = "seckill:order:lock:%s:%s";

       /**
        * 秒杀商品详情Key
        * 格式:seckill:product:{productId}
        * 类型:Hash
        */
       public static final String SECKILL_PRODUCT = "seckill:product:%s";

       /**
        * 秒杀活动列表Key
        * 格式:seckill:activity:list
        * 类型:List
        */
       public static final String SECKILL_ACTIVITY_LIST = "seckill:activity:list";
    }

    3.2 Lua脚本:原子扣减库存

    — seckill_stock_deduct.lua
    — 秒杀库存扣减Lua脚本(原子操作)

    — 参数:KEYS[1] = 库存Key, KEYS[2] = 用户购买记录Key
    — 参数:ARGV[1] = 用户ID, ARGV[2] = 限购数量

    — 1. 检查库存是否充足
    local stock = tonumber(redis.call('get', KEYS[1]))
    if stock == nil or stock <= 0 then
       return -1  — 库存不足
    end

    — 2. 检查用户是否已购买
    local userBuyKey = KEYS[2]
    local userBuyCount = tonumber(redis.call('get', userBuyKey))
    if userBuyCount ~= nil and userBuyCount >= tonumber(ARGV[2]) then
       return -2  — 超过限购数量
    end

    — 3. 扣减库存
    redis.call('decr', KEYS[1])

    — 4. 增加用户购买记录
    if userBuyCount == nil then
       redis.call('set', userBuyKey, 1)
    else
       redis.call('incr', userBuyKey)
    end

    — 5. 设置用户购买记录过期时间(活动结束后24小时过期)
    redis.call('expire', userBuyKey, 86400)

    return 1  — 扣减成功

    3.3 Lua脚本:库存回滚

    — seckill_stock_rollback.lua
    — 秒杀库存回滚Lua脚本

    — 参数:KEYS[1] = 库存Key, KEYS[2] = 用户购买记录Key
    — 参数:ARGV[1] = 回滚数量

    — 1. 回滚库存
    redis.call('incrby', KEYS[1], tonumber(ARGV[1]))

    — 2. 囏少用户购买记录
    local userBuyCount = tonumber(redis.call('get', KEYS[2]))
    if userBuyCount ~= nil and userBuyCount > 0 then
       redis.call('decr', KEYS[2])
    end

    return 1

    3.4 Redis操作Service

    package com.example.service;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.stereotype.Service;

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

    import static com.example.constant.RedisKeyConstant.*;

    /**
    * Redis秒杀操作Service
    */
    @Service
    public class RedisSeckillService {

       private static final Logger log = LoggerFactory.getLogger(RedisSeckillService.class);

       @Autowired
       private StringRedisTemplate redisTemplate;

       /**
        * 库存扣减Lua脚本
        */
       private DefaultRedisScript<Long> stockDeductScript;

       /**
        * 库存回滚Lua脚本
        */
       private DefaultRedisScript<Long> stockRollbackScript;

       @PostConstruct
       public void init() {
           // 初始化库存扣减脚本
           stockDeductScript = new DefaultRedisScript<>();
           stockDeductScript.setScriptText(STOCK_DEDUCT_LUA);
           stockDeductScript.setResultType(Long.class);

           // 初始化库存回滚脚本
           stockRollbackScript = new DefaultRedisScript<>();
           stockRollbackScript.setScriptText(STOCK_ROLLBACK_LUA);
           stockRollbackScript.setResultType(Long.class);
      }

       /**
        * 预热秒杀库存到Redis
        *
        * @param activityId 活动ID
        * @param stock     库存数量
        */
       public void warmUpStock(Long activityId, Integer stock) {
           String stockKey = String.format(SECKILL_STOCK, activityId);
           redisTemplate.opsForValue().set(stockKey, stock.toString());
           log.info("秒杀库存预热成功,活动ID:{},库存:{}", activityId, stock);
      }

       /**
        * 获取秒杀库存
        *
        * @param activityId 活动ID
        * @return 库存数量
        */
       public Integer getStock(Long activityId) {
           String stockKey = String.format(SECKILL_STOCK, activityId);
           String stock = redisTemplate.opsForValue().get(stockKey);
           return stock != null ? Integer.parseInt(stock) : 0;
      }

       /**
        * 原子扣减库存(使用Lua脚本)
        *
        * @param activityId   活动ID
        * @param userId       用户ID
        * @param limitPerUser 每人限购数量
        * @return 1-成功,-1-库存不足,-2-超过限购
        */
       public Long deductStock(Long activityId, Long userId, Integer limitPerUser) {
           String stockKey = String.format(SECKILL_STOCK, activityId);
           String userBuyKey = String.format(SECKILL_USER_BUY, activityId, userId);

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

           log.info("库存扣减结果,活动ID:{},用户ID:{},结果:{}", activityId, userId, result);
           return result;
      }

       /**
        * 回滚库存(使用Lua脚本)
        *
        * @param activityId 活动ID
        * @param userId     用户ID
        * @param quantity   回滚数量
        */
       public void rollbackStock(Long activityId, Long userId, Integer quantity) {
           String stockKey = String.format(SECKILL_STOCK, activityId);
           String userBuyKey = String.format(SECKILL_USER_BUY, activityId, userId);

           redisTemplate.execute(
                   stockRollbackScript,
                   Arrays.asList(stockKey, userBuyKey),
                   quantity.toString()
          );

           log.info("库存回滚成功,活动ID:{},用户ID:{},数量:{}", activityId, userId, quantity);
      }

       /**
        * 获取用户已购数量
        *
        * @param activityId 活动ID
        * @param userId     用户ID
        * @return 已购数量
        */
       public Integer getUserBuyCount(Long activityId, Long userId) {
           String userBuyKey = String.format(SECKILL_USER_BUY, activityId, userId);
           String count = redisTemplate.opsForValue().get(userBuyKey);
           return count != null ? Integer.parseInt(count) : 0;
      }

       /**
        * 尝试获取分布式锁
        *
        * @param activityId 活动ID
        * @param userId     用户ID
        * @param timeout   超时时间(秒)
        * @return 是否获取成功
        */
       public boolean tryLock(Long activityId, Long userId, Long timeout) {
           String lockKey = String.format(SECKILL_ORDER_LOCK, activityId, userId);
           Boolean result = redisTemplate.opsForValue()
                  .setIfAbsent(lockKey, "1", timeout, TimeUnit.SECONDS);
           return Boolean.TRUE.equals(result);
      }

       /**
        * 释放分布式锁
        *
        * @param activityId 活动ID
        * @param userId     用户ID
        */
       public void unlock(Long activityId, Long userId) {
           String lockKey = String.format(SECKILL_ORDER_LOCK, activityId, userId);
           redisTemplate.delete(lockKey);
      }

       // Lua脚本常量
       private static final String STOCK_DEDUCT_LUA = """
               — 秒杀库存扣减Lua脚本
               local stock = tonumber(redis.call('get', KEYS[1]))
               if stock == nil or stock <= 0 then
                   return -1
               end
               
               local userBuyKey = KEYS[2]
               local userBuyCount = tonumber(redis.call('get', userBuyKey))
               if userBuyCount ~= nil and userBuyCount >= tonumber(ARGV[2]) then
                   return -2
               end
               
               redis.call('decr', KEYS[1])
               
               if userBuyCount == nil then
                   redis.call('set', userBuyKey, 1)
               else
                   redis.call('incr', userBuyKey)
               end
               
               redis.call('expire', userBuyKey, 86400)
               
               return 1
               """;

       private static final String STOCK_ROLLBACK_LUA = """
               — 秒杀库存回滚Lua脚本
               redis.call('incrby', KEYS[1], tonumber(ARGV[1]))
               
               local userBuyCount = tonumber(redis.call('get', KEYS[2]))
               if userBuyCount ~= nil and userBuyCount > 0 then
                   redis.call('decr', KEYS[2])
               end
               
               return 1
               """;
    }


    四、消息队列异步下单

    4.1 RabbitMQ配置

    package com.example.config;

    import org.springframework.amqp.core.*;
    import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
    import org.springframework.amqp.rabbit.connection.ConnectionFactory;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    /**
    * RabbitMQ配置类
    */
    @Configuration
    public class RabbitMQConfig {

       /**
        * 秒杀订单队列
        */
       public static final String SECKILL_ORDER_QUEUE = "seckill.order.queue";

       /**
        * 秒死信队列(处理失败订单)
        */
       public static final String SECKILL_ORDER_DEAD_QUEUE = "seckill.order.dead.queue";

       /**
        * 秒杀订单交换机
        */
       public static final String SECKILL_ORDER_EXCHANGE = "seckill.order.exchange";

       /**
        * 秒杀订单路由键
        */
       public static final String SECKILL_ORDER_ROUTING_KEY = "seckill.order.create";

       /**
        * 定义秒杀订单队列
        */
       @Bean
       public Queue seckillOrderQueue() {
           return QueueBuilder.durable(SECKILL_ORDER_QUEUE)
                   // 绑定死信队列
                  .withArgument("x-dead-letter-exchange", SECKILL_ORDER_EXCHANGE)
                  .withArgument("x-dead-letter-routing-key", "seckill.order.dead")
                   // 队列过期时间(可选)
                  .withArgument("x-message-ttl", 300000) // 5分钟
                  .build();
      }

       /**
        * 定义死信队列
        */
       @Bean
       public Queue seckillOrderDeadQueue() {
           return QueueBuilder.durable(SECKILL_ORDER_DEAD_QUEUE).build();
      }

       /**
        * 定义交换机
        */
       @Bean
       public DirectExchange seckillOrderExchange() {
           return new DirectExchange(SECKILL_ORDER_EXCHANGE);
      }

       /**
        * 绑定队列到交换机
        */
       @Bean
       public Binding seckillOrderBinding() {
           return BindingBuilder.bind(seckillOrderQueue())
                  .to(seckillOrderExchange())
                  .with(SECKILL_ORDER_ROUTING_KEY);
      }

       /**
        * 绑定死信队列到交换机
        */
       @Bean
       public Binding seckillOrderDeadBinding() {
           return BindingBuilder.bind(seckillOrderDeadQueue())
                  .to(seckillOrderExchange())
                  .with("seckill.order.dead");
      }

       /**
        * 消息转换器(JSON格式)
        */
       @Bean
       public MessageConverter jsonMessageConverter() {
           return new Jackson2JsonMessageConverter();
      }

       /**
        * 配置RabbitTemplate
        */
       @Bean
       public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
           RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
           rabbitTemplate.setMessageConverter(jsonMessageConverter());
           
           // 设置消息确认回调
           rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
               if (!ack) {
                   // 消息发送失败,记录日志或重试
                   System.err.println("消息发送失败:" + cause);
              }
          });
           
           return rabbitTemplate;
      }
    }

    4.2 秒杀订单消息体

    package com.example.model;

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

    /**
    * 秒杀订单消息体
    */
    @Data
    public class SeckillOrderMessage implements Serializable {

       private static final long serialVersionUID = 1L;

       /**
        * 活动ID
        */
       private Long activityId;

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

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

       /**
        * 商品名称
        */
       private String productName;

       /**
        * 秒杀价
        */
       private BigDecimal seckillPrice;

       /**
        * 购买数量
        */
       private Integer quantity;

       /**
        * 订单创建时间
        */
       private Long createTime;
    }

    4.3 消息生产者

    package com.example.service;

    import com.example.config.RabbitMQConfig;
    import com.example.model.SeckillOrderMessage;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    /**
    * 秒杀消息生产者
    */
    @Service
    public class SeckillMessageProducer {

       private static final Logger log = LoggerFactory.getLogger(SeckillMessageProducer.class);

       @Autowired
       private RabbitTemplate rabbitTemplate;

       /**
        * 发送秒杀订单消息
        *
        * @param message 订单消息
        */
       public void sendSeckillOrderMessage(SeckillOrderMessage message) {
           try {
               rabbitTemplate.convertAndSend(
                       RabbitMQConfig.SECKILL_ORDER_EXCHANGE,
                       RabbitMQConfig.SECKILL_ORDER_ROUTING_KEY,
                       message
              );
               log.info("秒杀订单消息发送成功,活动ID:{},用户ID:{}",
                       message.getActivityId(), message.getUserId());
          } catch (Exception e) {
               log.error("秒杀订单消息发送失败,活动ID:{},用户ID:{}",
                       message.getActivityId(), message.getUserId(), e);
               throw new RuntimeException("消息发送失败", e);
          }
      }
    }

    4.4 消息消费者

    package com.example.consumer;

    import com.example.entity.SeckillOrder;
    import com.example.mapper.SeckillOrderMapper;
    import com.example.model.SeckillOrderMessage;
    import com.example.service.RedisSeckillService;
    import com.rabbitmq.client.Channel;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

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

    /**
    * 秒杀订单消息消费者
    */
    @Service
    public class SeckillOrderConsumer {

       private static final Logger log = LoggerFactory.getLogger(SeckillOrderConsumer.class);

       @Autowired
       private SeckillOrderMapper seckillOrderMapper;

       @Autowired
       private RedisSeckillService redisSeckillService;

       /**
        * 处理秒杀订单消息
        *
        * @param message 消息体
        * @param channel RabbitMQ Channel
        * @param msg     原始消息
        */
       @RabbitListener(queues = "seckill.order.queue")
       @Transactional(rollbackFor = Exception.class)
       public void handleSeckillOrder(SeckillOrderMessage message, Channel channel, Message msg) {
           log.info("开始处理秒杀订单消息,活动ID:{},用户ID:{}",
                   message.getActivityId(), message.getUserId());

           try {
               // 1. 检查用户是否已下单(数据库层面)
               Integer existOrder = seckillOrderMapper.countByActivityIdAndUserId(
                       message.getActivityId(), message.getUserId());
               if (existOrder > 0) {
                   log.warn("用户已下单,忽略消息,活动ID:{},用户ID:{}",
                           message.getActivityId(), message.getUserId());
                   // 确认消息
                   channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
                   return;
              }

               // 2. 扣减数据库库存(乐观锁)
               int updateResult = seckillOrderMapper.deductStock(message.getActivityId());
               if (updateResult <= 0) {
                   log.warn("数据库库存不足,活动ID:{}", message.getActivityId());
                   // 回滚Redis库存
                   redisSeckillService.rollbackStock(
                           message.getActivityId(),
                           message.getUserId(),
                           message.getQuantity());
                   // 确认消息
                   channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
                   return;
              }

               // 3. 创建订单
               SeckillOrder order = new SeckillOrder();
               order.setOrderNo(generateOrderNo());
               order.setActivityId(message.getActivityId());
               order.setUserId(message.getUserId());
               order.setProductId(message.getProductId());
               order.setProductName(message.getProductName());
               order.setSeckillPrice(message.getSeckillPrice());
               order.setQuantity(message.getQuantity());
               order.setTotalAmount(message.getSeckillPrice().multiply(new BigDecimal(message.getQuantity())));
               order.setStatus(0); // 待支付
               order.setCreateTime(LocalDateTime.now());

               seckillOrderMapper.insert(order);

               // 4. 确认消息
               channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
               log.info("秒杀订单创建成功,订单号:{},用户ID:{}", order.getOrderNo(), message.getUserId());

          } catch (Exception e) {
               log.error("处理秒杀订单消息异常,活动ID:{},用户ID:{}",
                       message.getActivityId(), message.getUserId(), e);
               try {
                   // 消息处理失败,拒绝消息并重新入队
                   channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, true);
              } catch (Exception ex) {
                   log.error("消息Nack失败", ex);
              }
          }
      }

       /**
        * 生成订单号
        */
       private String generateOrderNo() {
           return "SK" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8);
      }
    }


    五、秒杀核心Service

    5.1 秒杀活动Service

    package com.example.service;

    import com.example.entity.SeckillActivity;
    import com.example.mapper.SeckillActivityMapper;
    import com.example.model.SeckillOrderMessage;
    import com.example.vo.SeckillResult;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.time.LocalDateTime;
    import java.time.temporal.ChronoUnit;

    /**
    * 秒杀活动Service
    */
    @Service
    public class SeckillActivityService {

       private static final Logger log = LoggerFactory.getLogger(SeckillActivityService.class);

       @Autowired
       private SeckillActivityMapper seckillActivityMapper;

       @Autowired
       private RedisSeckillService redisSeckillService;

       @Autowired
       private SeckillMessageProducer messageProducer;

       /**
        * 获取秒杀活动详情
        *
        * @param activityId 活动ID
        * @return 活动信息
        */
       public SeckillActivity getActivity(Long activityId) {
           return seckillActivityMapper.selectById(activityId);
      }

       /**
        * 预热秒杀活动到Redis
        *
        * @param activityId 活动ID
        */
       public void warmUpActivity(Long activityId) {
           SeckillActivity activity = seckillActivityMapper.selectById(activityId);
           if (activity == null) {
               throw new RuntimeException("活动不存在");
          }

           // 预热库存
           redisSeckillService.warmUpStock(activityId, activity.getAvailableStock());

           log.info("秒杀活动预热成功,活动ID:{},库存:{}", activityId, activity.getAvailableStock());
      }

       /**
        * 执行秒杀
        *
        * @param activityId 活动ID
        * @param userId     用户ID
        * @return 秒杀结果
        */
       public SeckillResult executeSeckill(Long activityId, Long userId) {
           // 1. 参数校验
           if (activityId == null || userId == null) {
               return SeckillResult.fail("参数错误");
          }

           // 2. 获取活动信息
           SeckillActivity activity = seckillActivityMapper.selectById(activityId);
           if (activity == null) {
               return SeckillResult.fail("活动不存在");
          }

           // 3. 校验活动状态
           LocalDateTime now = LocalDateTime.now();
           if (now.isBefore(activity.getStartTime())) {
               return SeckillResult.fail("活动未开始");
          }
           if (now.isAfter(activity.getEndTime())) {
               return SeckillResult.fail("活动已结束");
          }
           if (activity.getStatus() != 1) {
               return SeckillResult.fail("活动状态异常");
          }

           // 4. 获取分布式锁(防止重复提交)
           boolean locked = redisSeckillService.tryLock(activityId, userId, 10L);
           if (!locked) {
               return SeckillResult.fail("请勿重复提交");
          }

           try {
               // 5. 扣减Redis库存(Lua脚本原子操作)
               Long deductResult = redisSeckillService.deductStock(
                       activityId, userId, activity.getLimitPerUser());

               if (deductResult == -1) {
                   return SeckillResult.fail("库存不足");
              }
               if (deductResult == -2) {
                   return SeckillResult.fail("超过限购数量");
              }

               // 6. 发送MQ消息,异步创建订单
               SeckillOrderMessage message = new SeckillOrderMessage();
               message.setActivityId(activityId);
               message.setUserId(userId);
               message.setProductId(activity.getProductId());
               message.setProductName(activity.getProductName());
               message.setSeckillPrice(activity.getSeckillPrice());
               message.setQuantity(1);
               message.setCreateTime(System.currentTimeMillis());

               messageProducer.sendSeckillOrderMessage(message);

               // 7. 返回成功
               return SeckillResult.success("秒杀成功,请尽快支付");

          } catch (Exception e) {
               log.error("秒杀执行异常,活动ID:{},用户ID:{}", activityId, userId, e);
               // 回滚库存
               redisSeckillService.rollbackStock(activityId, userId, 1);
               return SeckillResult.fail("系统繁忙,请稍后重试");
          } finally {
               // 8. 释放锁
               redisSeckillService.unlock(activityId, userId);
          }
      }

       /**
        * 获取秒杀活动状态
        *
        * @param activityId 活动ID
        * @return 活动状态信息
        */
       public SeckillActivityStatus getActivityStatus(Long activityId) {
           SeckillActivity activity = seckillActivityMapper.selectById(activityId);
           if (activity == null) {
               return null;
          }

           SeckillActivityStatus status = new SeckillActivityStatus();
           status.setActivityId(activityId);
           status.setStatus(activity.getStatus());
           status.setStartTime(activity.getStartTime());
           status.setEndTime(activity.getEndTime());

           // 获取Redis中的实时库存
           Integer stock = redisSeckillService.getStock(activityId);
           status.setAvailableStock(stock);

           // 计算剩余时间
           LocalDateTime now = LocalDateTime.now();
           if (now.isBefore(activity.getStartTime())) {
               // 未开始,计算倒计时
               long seconds = ChronoUnit.SECONDS.between(now, activity.getStartTime());
               status.setCountdown(seconds);
               status.setPhase("NOT_START");
          } else if (now.isAfter(activity.getEndTime())) {
               // 已结束
               status.setCountdown(0L);
               status.setPhase("ENDED");
          } else {
               // 进行中
               long seconds = ChronoUnit.SECONDS.between(now, activity.getEndTime());
               status.setCountdown(seconds);
               status.setPhase("IN_PROGRESS");
          }

           return status;
      }

       /**
        * 秒杀活动状态内部类
        */
       @lombok.Data
       public static class SeckillActivityStatus {
           private Long activityId;
           private Integer status;
           private LocalDateTime startTime;
           private LocalDateTime endTime;
           private Integer availableStock;
           private Long countdown;
           private String phase;
      }
    }

    5.2 秒杀结果VO

    package com.example.vo;

    import lombok.Data;

    /**
    * 秒杀结果
    */
    @Data
    public class SeckillResult {

       /**
        * 状态码:200-成功,500-失败
        */
       private Integer code;

       /**
        * 消息
        */
       private String message;

       /**
        * 数据(可选)
        */
       private Object data;

       public static SeckillResult success(String message) {
           SeckillResult result = new SeckillResult();
           result.setCode(200);
           result.setMessage(message);
           return result;
      }

       public static SeckillResult fail(String message) {
           SeckillResult result = new SeckillResult();
           result.setCode(500);
           result.setMessage(message);
           return result;
      }

       public static SeckillResult success(String message, Object data) {
           SeckillResult result = new SeckillResult();
           result.setCode(200);
           result.setMessage(message);
           result.setData(data);
           return result;
      }
    }


    六、限流与防刷策略

    6.1 限流注解

    package com.example.annotation;

    import java.lang.annotation.*;

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

       /**
        * 限流key(支持SpEL表达式)
        */
       String key() default "";

       /**
        * 限流次数
        */
       int count() default 100;

       /**
        * 限流时间窗口(秒)
        */
       int time() default 1;

       /**
        * 限流类型
        */
       LimitType limitType() default LimitType.IP;

       /**
        * 限流类型枚举
        */
       enum LimitType {
           /**
            * 按IP限流
            */
           IP,
           /**
            * 按用户限流
            */
           USER,
           /**
            * 按接口限流
            */
           GLOBAL
      }
    }

    6.2 限流切面

    package com.example.aspect;

    import com.example.annotation.RateLimit;
    import com.example.exception.RateLimitException;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;

    import javax.annotation.PostConstruct;
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.util.Collections;

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

       private static final Logger log = LoggerFactory.getLogger(RateLimitAspect.class);

       @Autowired
       private StringRedisTemplate redisTemplate;

       private DefaultRedisScript<Long> rateLimitScript;

       @PostConstruct
       public void init() {
           rateLimitScript = new DefaultRedisScript<>();
           rateLimitScript.setScriptText(RATE_LIMIT_LUA);
           rateLimitScript.setResultType(Long.class);
      }

       @Before("@annotation(rateLimit)")
       public void doBefore(JoinPoint point, RateLimit rateLimit) {
           // 获取限流key
           String key = getLimitKey(point, rateLimit);

           // 执行限流Lua脚本
           Long count = redisTemplate.execute(
                   rateLimitScript,
                   Collections.singletonList(key),
                   String.valueOf(rateLimit.count()),
                   String.valueOf(rateLimit.time())
          );

           if (count == null || count > rateLimit.count()) {
               log.warn("触发限流,key:{},当前次数:{}", key, count);
               throw new RateLimitException("请求过于频繁,请稍后重试");
          }
      }

       /**
        * 生成限流key
        */
       private String getLimitKey(JoinPoint point, RateLimit rateLimit) {
           StringBuilder key = new StringBuilder("rate_limit:");

           // 根据限流类型生成key
           switch (rateLimit.limitType()) {
               case IP:
                   key.append(getIpAddress()).append(":");
                   break;
               case USER:
                   // 从请求中获取用户ID(根据实际项目调整)
                   key.append(getUserId()).append(":");
                   break;
               case GLOBAL:
                   key.append("global:");
                   break;
          }

           // 获取方法签名
           MethodSignature signature = (MethodSignature) point.getSignature();
           Method method = signature.getMethod();
           key.append(method.getDeclaringClass().getName())
              .append(".")
              .append(method.getName());

           return key.toString();
      }

       /**
        * 获取请求IP地址
        */
       private String getIpAddress() {
           ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
           if (attributes == null) {
               return "unknown";
          }
           HttpServletRequest request = attributes.getRequest();
           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();
          }
           // 多个代理时取第一个
           if (ip != null && ip.contains(",")) {
               ip = ip.split(",")[0].trim();
          }
           return ip;
      }

       /**
        * 获取用户ID(需要根据实际项目实现)
        */
       private String getUserId() {
           // TODO: 从SecurityContext或Token中获取用户ID
           return "anonymous";
      }

       /**
        * 限流Lua脚本
        */
       private static final String RATE_LIMIT_LUA = """
               local key = KEYS[1]
               local limit = tonumber(ARGV[1])
               local expire = tonumber(ARGV[2])
               
               local count = tonumber(redis.call('get', key) or "0")
               
               if count >= limit then
                   return count + 1
               end
               
               count = redis.call('incr', key)
               if count == 1 then
                   redis.call('expire', key, expire)
               end
               
               return count
               """;
    }

    6.3 验证码防刷

    package com.example.service;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;

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

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

       private static final Logger log = LoggerFactory.getLogger(CaptchaService.class);

       @Autowired
       private StringRedisTemplate redisTemplate;

       /**
        * 验证码过期时间(秒)
        */
       private static final long CAPTCHA_EXPIRE_SECONDS = 300;

       /**
        * 验证码Redis key前缀
        */
       private static final String CAPTCHA_KEY_PREFIX = "captcha:seckill:";

       /**
        * 生成验证码
        *
        * @param userId 用户ID
        * @return 验证码图片Base64和验证码ID
        */
       public CaptchaResult generateCaptcha(Long userId) {
           // 生成随机验证码
           String captchaCode = generateRandomCode(4);
           
           // 生成验证码ID
           String captchaId = String.valueOf(System.currentTimeMillis());

           // 保存到Redis
           String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
           redisTemplate.opsForValue().set(redisKey, captchaCode, CAPTCHA_EXPIRE_SECONDS, TimeUnit.SECONDS);

           // 生成验证码图片
           String base64Image = generateCaptchaImage(captchaCode);

           log.info("验证码生成成功,用户ID:{},验证码ID:{}", userId, captchaId);

           CaptchaResult result = new CaptchaResult();
           result.setCaptchaId(captchaId);
           result.setCaptchaImage(base64Image);
           return result;
      }

       /**
        * 验证验证码
        *
        * @param captchaId   验证码ID
        * @param captchaCode 用户输入的验证码
        * @return 是否验证成功
        */
       public boolean verifyCaptcha(String captchaId, String captchaCode) {
           if (captchaId == null || captchaCode == null) {
               return false;
          }

           String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
           String storedCode = redisTemplate.opsForValue().get(redisKey);

           if (storedCode == null) {
               log.warn("验证码已过期或不存在,captchaId:{}", captchaId);
               return false;
          }

           // 验证成功后删除验证码
           if (storedCode.equalsIgnoreCase(captchaCode)) {
               redisTemplate.delete(redisKey);
               log.info("验证码验证成功,captchaId:{}", captchaId);
               return true;
          }

           log.warn("验证码错误,captchaId:{},输入:{},正确:{}", captchaId, captchaCode, storedCode);
           return false;
      }

       /**
        * 生成随机验证码
        */
       private String generateRandomCode(int length) {
           String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
           StringBuilder sb = new StringBuilder();
           Random random = new Random();
           for (int i = 0; i < length; i++) {
               sb.append(chars.charAt(random.nextInt(chars.length())));
          }
           return sb.toString();
      }

       /**
        * 生成验证码图片
        */
       private String generateCaptchaImage(String code) {
           int width = 120;
           int height = 40;
           BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
           Graphics2D g = image.createGraphics();

           // 设置背景色
           g.setColor(Color.WHITE);
           g.fillRect(0, 0, width, height);

           // 设置字体
           g.setFont(new Font("Arial", Font.BOLD, 24));

           // 绘制干扰线
           Random random = new Random();
           g.setColor(Color.LIGHT_GRAY);
           for (int i = 0; i < 20; i++) {
               int x1 = random.nextInt(width);
               int y1 = random.nextInt(height);
               int x2 = random.nextInt(width);
               int y2 = random.nextInt(height);
               g.drawLine(x1, y1, x2, y2);
          }

           // 绘制验证码
           for (int i = 0; i < code.length(); i++) {
               g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100)));
               g.drawString(String.valueOf(code.charAt(i)), 20 + i * 25, 28);
          }

           g.dispose();

           // 转换为Base64
           try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
               ImageIO.write(image, "PNG", baos);
               byte[] bytes = baos.toByteArray();
               return "data:image/png;base64," + Base64.getEncoder().encodeToString(bytes);
          } catch (IOException e) {
               log.error("生成验证码图片失败", e);
               return "";
          }
      }

       /**
        * 验证码结果内部类
        */
       @lombok.Data
       public static class CaptchaResult {
           private String captchaId;
           private String captchaImage;
      }
    }


    七、秒杀Controller

    package com.example.controller;

    import com.example.annotation.RateLimit;
    import com.example.entity.SeckillActivity;
    import com.example.service.CaptchaService;
    import com.example.service.SeckillActivityService;
    import com.example.service.SeckillActivityService.SeckillActivityStatus;
    import com.example.service.RedisSeckillService;
    import com.example.vo.SeckillResult;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 秒杀Controller
    */
    @RestController
    @RequestMapping("/api/seckill")
    public class SeckillController {

       private static final Logger log = LoggerFactory.getLogger(SeckillController.class);

       @Autowired
       private SeckillActivityService seckillActivityService;

       @Autowired
       private CaptchaService captchaService;

       @Autowired
       private RedisSeckillService redisSeckillService;

       /**
        * 获取秒杀活动详情
        */
       @GetMapping("/activity/{activityId}")
       public Map<String, Object> getActivity(@PathVariable Long activityId) {
           Map<String, Object> result = new HashMap<>();
           try {
               SeckillActivity activity = seckillActivityService.getActivity(activityId);
               if (activity == null) {
                   result.put("code", 404);
                   result.put("message", "活动不存在");
                   return result;
              }

               result.put("code", 200);
               result.put("data", activity);
          } catch (Exception e) {
               result.put("code", 500);
               result.put("message", "查询失败");
          }
           return result;
      }

       /**
        * 获取秒杀活动状态(实时库存、倒计时等)
        */
       @GetMapping("/activity/{activityId}/status")
       public Map<String, Object> getActivityStatus(@PathVariable Long activityId) {
           Map<String, Object> result = new HashMap<>();
           try {
               SeckillActivityStatus status = seckillActivityService.getActivityStatus(activityId);
               if (status == null) {
                   result.put("code", 404);
                   result.put("message", "活动不存在");
                   return result;
              }

               result.put("code", 200);
               result.put("data", status);
          } catch (Exception e) {
               result.put("code", 500);
               result.put("message", "查询失败");
          }
           return result;
      }

       /**
        * 获取秒杀验证码
        */
       @GetMapping("/captcha")
       @RateLimit(count = 10, time = 60, limitType = RateLimit.LimitType.IP)
       public Map<String, Object> getCaptcha(@RequestParam Long userId) {
           Map<String, Object> result = new HashMap<>();
           try {
               CaptchaService.CaptchaResult captcha = captchaService.generateCaptcha(userId);
               result.put("code", 200);
               result.put("data", captcha);
          } catch (Exception e) {
               result.put("code", 500);
               result.put("message", "获取验证码失败");
          }
           return result;
      }

       /**
        * 执行秒杀
        *
        * @param activityId 活动ID
        * @param userId     用户ID
        * @param captchaId 验证码ID
        * @param captchaCode 验证码
        */
       @PostMapping("/execute")
       @RateLimit(count = 5, time = 1, limitType = RateLimit.LimitType.USER)
       public SeckillResult executeSeckill(@RequestParam Long activityId,
                                            @RequestParam Long userId,
                                            @RequestParam String captchaId,
                                            @RequestParam String captchaCode) {
           // 1. 验证验证码
           boolean captchaValid = captchaService.verifyCaptcha(captchaId, captchaCode);
           if (!captchaValid) {
               return SeckillResult.fail("验证码错误或已过期");
          }

           // 2. 执行秒杀
           return seckillActivityService.executeSeckill(activityId, userId);
      }

       /**
        * 预热秒杀活动(管理员接口)
        */
       @PostMapping("/activity/{activityId}/warmup")
       public Map<String, Object> warmUpActivity(@PathVariable Long activityId) {
           Map<String, Object> result = new HashMap<>();
           try {
               seckillActivityService.warmUpActivity(activityId);
               result.put("code", 200);
               result.put("message", "预热成功");
          } catch (Exception e) {
               result.put("code", 500);
               result.put("message", "预热失败:" + e.getMessage());
          }
           return result;
      }
    }


    八、前端秒杀页面示例

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
       <title>秒杀活动</title>
       <style>
          * {
               margin: 0;
               padding: 0;
               box-sizing: border-box;
          }
           body {
               font-family: 'Microsoft YaHei', sans-serif;
               background: #f5f5f5;
          }
           .container {
               max-width: 800px;
               margin: 0 auto;
               padding: 20px;
          }
           .seckill-card {
               background: white;
               border-radius: 10px;
               padding: 20px;
               margin-bottom: 20px;
               box-shadow: 0 2px 10px rgba(0,0,0,0.1);
          }
           .product-info {
               display: flex;
               gap: 20px;
          }
           .product-image {
               width: 200px;
               height: 200px;
               object-fit: cover;
               border-radius: 5px;
          }
           .product-detail {
               flex: 1;
          }
           .product-name {
               font-size: 20px;
               font-weight: bold;
               margin-bottom: 10px;
          }
           .price-info {
               margin: 15px 0;
          }
           .original-price {
               text-decoration: line-through;
               color: #999;
               font-size: 16px;
          }
           .seckill-price {
               color: #ff4444;
               font-size: 28px;
               font-weight: bold;
               margin-left: 10px;
          }
           .stock-info {
               color: #666;
               margin-bottom: 20px;
          }
           .countdown {
               background: #ff4444;
               color: white;
               padding: 10px 20px;
               border-radius: 5px;
               display: inline-block;
               margin-bottom: 20px;
          }
           .countdown span {
               font-weight: bold;
               font-size: 20px;
          }
           .seckill-btn {
               background: #ff4444;
               color: white;
               border: none;
               padding: 15px 40px;
               font-size: 18px;
               border-radius: 5px;
               cursor: pointer;
               width: 100%;
          }
           .seckill-btn:hover {
               background: #ff2222;
          }
           .seckill-btn:disabled {
               background: #ccc;
               cursor: not-allowed;
          }
           .captcha-modal {
               display: none;
               position: fixed;
               top: 0;
               left: 0;
               width: 100%;
               height: 100%;
               background: rgba(0,0,0,0.5);
               z-index: 1000;
          }
           .captcha-content {
               background: white;
               width: 350px;
               padding: 30px;
               border-radius: 10px;
               position: absolute;
               top: 50%;
               left: 50%;
               transform: translate(-50%, -50%);
          }
           .captcha-image {
               width: 120px;
               height: 40px;
               cursor: pointer;
               vertical-align: middle;
          }
           .captcha-input {
               padding: 10px;
               font-size: 16px;
               border: 1px solid #ddd;
               border-radius: 5px;
               width: 150px;
          }
           .result-message {
               text-align: center;
               padding: 20px;
               font-size: 18px;
          }
           .success {
               color: #52c41a;
          }
           .fail {
               color: #ff4444;
          }
       </style>
    </head>
    <body>
       <div class="container">
           <div class="seckill-card">
               <div class="product-info">
                   <img src="product.jpg" alt="商品图片" class="product-image">
                   <div class="product-detail">
                       <h2 class="product-name">iPhone 15 Pro Max 256GB</h2>
                       <div class="price-info">
                           <span class="original-price">¥9999</span>
                           <span class="seckill-price">¥7999</span>
                       </div>
                       <div class="stock-info">
                          剩余库存:<span id="stock">100</span> 件
                       </div>
                       <div class="countdown">
                          距离开始:<span id="countdown">00:00:00</span>
                       </div>
                       <button class="seckill-btn" id="seckillBtn" οnclick="showCaptcha()">
                          立即秒杀
                       </button>
                   </div>
               </div>
           </div>
       </div>

       <!– 验证码弹窗 –>
       <div class="captcha-modal" id="captchaModal">
           <div class="captcha-content">
               <h3 style="margin-bottom: 20px;">请输入验证码</h3>
               <div style="margin-bottom: 15px;">
                   <img id="captchaImage" class="captcha-image" οnclick="refreshCaptcha()" title="点击刷新">
                   <input type="text" id="captchaCode" class="captcha-input" placeholder="请输入验证码" maxlength="4">
               </div>
               <div style="text-align: center;">
                   <button οnclick="executeSeckill()" style="padding: 10px 30px; margin-right: 10px;">确认</button>
                   <button οnclick="closeCaptcha()" style="padding: 10px 30px;">取消</button>
               </div>
           </div>
       </div>

       <!– 结果弹窗 –>
       <div class="captcha-modal" id="resultModal">
           <div class="captcha-content">
               <div class="result-message" id="resultMessage"></div>
               <div style="text-align: center; margin-top: 20px;">
                   <button οnclick="closeResult()" style="padding: 10px 30px;">确定</button>
               </div>
           </div>
       </div>

       <script>
           let captchaId = '';
           const userId = 10001; // 当前登录用户ID
           const activityId = 1; // 秒杀活动ID

           // 倒计时功能
           function updateCountdown() {
               // 这里应该从后端获取实际的倒计时
               // 简化示例:模拟倒计时
               const countdownElement = document.getElementById('countdown');
               let timeStr = countdownElement.textContent;
               let parts = timeStr.split(':');
               let seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
               
               if (seconds > 0) {
                   seconds–;
                   const hours = Math.floor(seconds / 3600);
                   const mins = Math.floor((seconds % 3600) / 60);
                   const secs = seconds % 60;
                   countdownElement.textContent =
                       String(hours).padStart(2, '0') + ':' +
                       String(mins).padStart(2, '0') + ':' +
                       String(secs).padStart(2, '0');
              }
          }
           setInterval(updateCountdown, 1000);

           // 显示验证码弹窗
           function showCaptcha() {
               document.getElementById('captchaModal').style.display = 'block';
               refreshCaptcha();
          }

           // 关闭验证码弹窗
           function closeCaptcha() {
               document.getElementById('captchaModal').style.display = 'none';
          }

           // 刷新验证码
           async function refreshCaptcha() {
               try {
                   const response = await fetch(`/api/seckill/captcha?userId=${userId}`);
                   const data = await response.json();
                   if (data.code === 200) {
                       captchaId = data.data.captchaId;
                       document.getElementById('captchaImage').src = data.data.captchaImage;
                  }
              } catch (error) {
                   console.error('获取验证码失败', error);
              }
          }

           // 执行秒杀
           async function executeSeckill() {
               const captchaCode = document.getElementById('captchaCode').value;
               if (!captchaCode) {
                   alert('请输入验证码');
                   return;
              }

               try {
                   const response = await fetch('/api/seckill/execute', {
                       method: 'POST',
                       headers: {
                           'Content-Type': 'application/x-www-form-urlencoded',
                      },
                       body: `activityId=${activityId}&userId=${userId}&captchaId=${captchaId}&captchaCode=${captchaCode}`
                  });
                   const data = await response.json();
                   
                   closeCaptcha();
                   showResult(data.code === 200, data.message);
                   
                   if (data.code === 200) {
                       // 秒杀成功,更新库存显示
                       const stockElement = document.getElementById('stock');
                       stockElement.textContent = parseInt(stockElement.textContent) – 1;
                  }
              } catch (error) {
                   console.error('秒杀请求失败', error);
                   showResult(false, '网络错误,请重试');
              }
          }

           // 显示结果
           function showResult(success, message) {
               const resultMessage = document.getElementById('resultMessage');
               resultMessage.className = success ? 'result-message success' : 'result-message fail';
               resultMessage.textContent = message;
               document.getElementById('resultModal').style.display = 'block';
          }

           // 关闭结果弹窗
           function closeResult() {
               document.getElementById('resultModal').style.display = 'none';
               document.getElementById('captchaCode').value = '';
          }

           // 定时刷新库存
           setInterval(async () => {
               try {
                   const response = await fetch(`/api/seckill/activity/${activityId}/status`);
                   const data = await response.json();
                   if (data.code === 200) {
                       document.getElementById('stock').textContent = data.data.availableStock;
                  }
              } catch (error) {
                   console.error('刷新库存失败', error);
              }
          }, 5000);
       </script>
    </body>
    </html>


    九、压力测试

    9.1 JMeter测试计划

    使用JMeter进行秒杀压力测试:

  • 线程组配置

    • 线程数:1000

    • Ramp-Up时间:1秒

    • 循环次数:1

  • HTTP请求配置

    • 方法:POST

    • 路径:/api/seckill/execute

    • 参数:

      • activityId: 1

      • userId: ${__Random(1,10000)}

      • captchaId: ${captchaId}

      • captchaCode: ${captchaCode}

  • 查看结果树

    • 监听器:查看结果树、聚合报告

  • 9.2 预期测试结果

    指标预期值
    并发数 1000
    响应时间 < 100ms
    TPS > 500
    错误率 < 1%
    库存扣减准确性 100%

    十、常见问题及解决方案

    10.1 超卖问题

    问题描述:多个请求同时扣减库存,导致库存为负数。

    解决方案:

  • Redis Lua脚本原子操作(本文采用)

  • 数据库乐观锁:UPDATE stock SET count = count – 1 WHERE id = ? AND count > 0

  • 分布式锁(性能较差)

  • 10.2 重复下单

    问题描述:同一用户多次点击秒杀按钮,产生多个订单。

    解决方案:

  • 分布式锁(本文采用)

  • 数据库唯一索引:UNIQUE KEY (activity_id, user_id)

  • Redis用户购买记录

  • 10.3 缓存与数据库不一致

    问题描述:Redis库存与数据库库存不一致。

    解决方案:

  • 异步消息保证最终一致性(本文采用)

  • 定时任务校对库存

  • 数据库操作失败时回滚Redis

  • 10.4 恶意刷单

    问题描述:脚本批量请求,影响正常用户。

    解决方案:

  • 验证码(本文采用)

  • IP限流

  • 用户行为分析

  • 设备指纹


  • 十一、优化建议

    11.1 性能优化

  • 使用本地缓存:Caffeine缓存活动信息,减少Redis访问

  • 连接池优化:Redis、MySQL连接池配置

  • 异步日志:使用Logback异步Appender

  • JVM调优:合理的堆大小和GC策略

  • 11.2 架构优化

  • 服务拆分:秒杀服务独立部署,不影响主站

  • 多级缓存:Nginx缓存 → 本地缓存 → Redis → 数据库

  • CDN加速:静态资源使用CDN

  • 弹性扩容:K8s自动扩容

  • 11.3 业务优化

  • 错峰秒杀:不同商品不同时间开始

  • 排队机制:超过库存的请求进入排队

  • 候补机制:未支付订单释放后,候补用户获得机会


  • 十二、总结

    本文实现了一个完整的秒杀系统,核心设计包括:

    模块技术方案作用
    库存预热 Redis 减少数据库访问
    库存扣减 Redis + Lua 原子操作,防止超卖
    异步下单 RabbitMQ 削峰填谷,保护数据库
    重复下单 分布式锁 + 唯一索引 保证一人一单
    限流防刷 注解 + Lua 保护系统稳定性
    验证码 Redis + 图片 防止脚本刷单

    关键技术点:

  • Redis Lua脚本保证原子性

  • 消息队列异步处理

  • 分布式锁防并发

  • 多级限流保护系统


  • 参考资料

    • Redis官方文档

    • RabbitMQ官方文档

    • Spring Boot官方文档

    • 高并发秒杀系统设计

    赞(0)
    未经允许不得转载:171主机测评 » Java高并发秒杀系统设计与实现(完整版)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址