SpringBoot+Redis实现电商秒杀方案

一、系统目标

在整个秒杀活动中的核心稳定性目标主要有以下几点:

  1. 系统不崩溃:保证即使在峰值流量下系统依然可用。
  2. 数据一致性:保证库存准确性,不出现超卖现象。
  3. 高性能:保证绝大多数请求在100毫秒内响应。

二、系统架构设计

bash 复制代码
前端页面 → 网关层 → 业务层 → Redis → 消息队列 → 数据库
                 (库存校验)        (异步下单)
2.1 分层削峰
  1. 前端层面:

    • 目标: 拦截80%无效请求
    • 措施:
      • 按钮防重复点击
      • 验证码校验
      • 活动未开始前端限制
  2. 网关层面:

    • 目标: 过滤90%恶意请求
    • 措施:
      • IP限流
      • 用户限流
      • 恶意请求识别
  3. 服务层面:

    • 目标: 平稳处理核心业务
    • 措施:
      • 令牌桶限流
      • 请求队列化
      • 异步处理
2.2 防超卖核心策略
  1. Redis原子操作:保证库存扣减的原子性
  2. 分布式锁:防止同一用户重复抢购
  3. 库存预热:活动开始前将库存加载到Redis
  4. 异步下单:快速响应,后端异步处理订单

三、代码实现

3.1 实体类定义
bash 复制代码
// 秒杀活动实体
@Data
public class SeckillActivity {
    private Long id;
    private String name;
    private Long productId;
    private BigDecimal seckillPrice;
    private Integer stock;
    private Integer initialStock;
    private Date startTime;
    private Date endTime;
    private Integer status; // 0-未开始 1-进行中 2-已结束
}

// 秒杀订单实体
@Data
public class SeckillOrder {
    private Long id;
    private Long userId;
    private Long activityId;
    private Long productId;
    private String orderNo;
    private BigDecimal amount;
    private Integer quantity;
    private Integer status; // 0-待支付 1-已支付 2-已取消
    private Date createTime;
}
3.2 Redis Key管理
bash 复制代码
@Component
public class RedisKeyManager {
    
    // 秒杀库存KEY
    public static String getStockKey(Long activityId) {
        return "seckill:stock:" + activityId;
    }
    
    // 用户秒杀记录KEY(防重复抢购)
    public static String getUserSeckillKey(Long activityId, Long userId) {
        return "seckill:user:" + activityId + ":" + userId;
    }
    
    // 秒杀活动信息KEY
    public static String getActivityKey(Long activityId) {
        return "seckill:activity:" + activityId;
    }
    
    // 分布式锁KEY
    public static String getSeckillLockKey(Long activityId) {
        return "seckill:lock:" + activityId;
    }
}
3.3 库存预热服务
bash 复制代码
@Service
@Slf4j
public class SeckillPreheatService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private SeckillActivityMapper activityMapper;
    
    /**
     * 预热秒杀库存到Redis
     */
    public void preheatStock(Long activityId) {
        SeckillActivity activity = activityMapper.selectById(activityId);
        if (activity == null) {
            throw new RuntimeException("秒杀活动不存在");
        }
        
        String stockKey = RedisKeyManager.getStockKey(activityId);
        String activityKey = RedisKeyManager.getActivityKey(activityId);
        
        // 设置库存
        redisTemplate.opsForValue().set(stockKey, activity.getStock());
        
        // 缓存活动信息
        redisTemplate.opsForValue().set(activityKey, activity);
        
        log.info("秒杀活动{}库存预热完成,库存量:{}", activityId, activity.getStock());
    }
    
    /**
     * 获取Redis中的库存
     */
    public Integer getStockFromRedis(Long activityId) {
        String stockKey = RedisKeyManager.getStockKey(activityId);
        Object stockObj = redisTemplate.opsForValue().get(stockKey);
        return stockObj != null ? Integer.parseInt(stockObj.toString()) : 0;
    }
}
3.4 核心秒杀服务(防超卖关键)
bash 复制代码
@Service
@Slf4j
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 方案一:使用Redis原子操作扣减库存(推荐)
     * 利用Redis单线程特性,保证原子性
     */
    public SeckillResult seckillV1(Long activityId, Long userId) {
        // 1. 参数校验
        if (activityId == null || userId == null) {
            return SeckillResult.error("参数错误");
        }
        
        // 2. 校验用户是否重复参与
        String userSeckillKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
        if (Boolean.TRUE.equals(redisTemplate.hasKey(userSeckillKey))) {
            return SeckillResult.error("请勿重复参与秒杀");
        }
        
        // 3. 原子扣减库存
        String stockKey = RedisKeyManager.getStockKey(activityId);
        Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
        
        if (remainStock == null) {
            return SeckillResult.error("秒杀活动不存在");
        }
        
        if (remainStock < 0) {
            // 库存不足,恢复库存
            // 这里恢复库存意图是好的,为了确保库存不会变成负数
            // 在极高并发下,可能会出现多个线程都扣减了库存,然后都发现库存为负数,
            然后都恢复了库存。这样,库存最终恢复到了0,但是实际上这些线程都没有成功下单。
            所以,并没有超卖。
            // 但是在这个过程中, 每次扣减都要操作Redis两次(decrement和increment),
            增加了Redis的负担,并不是个完美的方案。
            redisTemplate.opsForValue().increment(stockKey);
            return SeckillResult.error("商品已售罄");
        }
        
        try {
            // 4. 记录用户秒杀成功
            redisTemplate.opsForValue().set(userSeckillKey, "1", Duration.ofMinutes(30));
            
            // 5. 发送异步下单消息
            sendSeckillOrderMessage(activityId, userId);
            
            return SeckillResult.success("秒杀成功,请尽快支付");
            
        } catch (Exception e) {
            // 异常情况恢复库存
            redisTemplate.opsForValue().increment(stockKey);
            redisTemplate.delete(userSeckillKey);
            log.error("秒杀异常", e);
            return SeckillResult.error("系统繁忙,请重试");
        }
    }
    
    /**
     * 方案二:使用Lua脚本保证原子性(更安全)
     */
    public SeckillResult seckillV2(Long activityId, Long userId) {
        String luaScript = 
            "local stockKey = KEYS[1] " +
            "local userKey = KEYS[2] " +
            "local activityId = ARGV[1] " +
            "local userId = ARGV[2] " +
            
            // 检查用户是否已参与
            "if redis.call('exists', userKey) == 1 then " +
            "   return 2 " +
            "end " +
            
            // 检查库存
            "local stock = tonumber(redis.call('get', stockKey)) " +
            "if not stock or stock <= 0 then " +
            "   return 3 " +
            "end " +
            
            // 扣减库存
            "redis.call('decr', stockKey) " +
            
            // 记录用户参与
            "redis.call('setex', userKey, 1800, 1) " +
            
            "return 1 ";
        
        List<String> keys = Arrays.asList(
            RedisKeyManager.getStockKey(activityId),
            RedisKeyManager.getUserSeckillKey(activityId, userId)
        );
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, keys, activityId.toString(), userId.toString());
        
        switch (result.intValue()) {
            case 1:
                sendSeckillOrderMessage(activityId, userId);
                return SeckillResult.success("秒杀成功");
            case 2:
                return SeckillResult.error("请勿重复参与");
            case 3:
                return SeckillResult.error("商品已售罄");
            default:
                return SeckillResult.error("秒杀失败");
        }
    }
    
    /**
     * 方案三:分布式锁 + Redis原子操作(最安全,适合极端高并发)
     */
    public SeckillResult seckillV3(Long activityId, Long userId) {
        String lockKey = RedisKeyManager.getSeckillLockKey(activityId);
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待100ms,锁有效期30秒
            boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
            if (!locked) {
                return SeckillResult.error("系统繁忙,请重试");
            }
            
            // 执行秒杀逻辑
            return seckillV2(activityId, userId);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return SeckillResult.error("系统异常");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    /**
     * 发送秒杀订单消息到消息队列
     */
    private void sendSeckillOrderMessage(Long activityId, Long userId) {
        Map<String, Object> message = new HashMap<>();
        message.put("activityId", activityId);
        message.put("userId", userId);
        message.put("createTime", System.currentTimeMillis());
        
        rabbitTemplate.convertAndSend("seckill.order.exchange", 
                                   "seckill.order.route", 
                                   message);
        log.info("发送秒杀订单消息: activityId={}, userId={}", activityId, userId);
    }
}
3.5 消息队列消费者(异步下单)
bash 复制代码
@Component
@Slf4j
public class SeckillOrderConsumer {
    
    @Autowired
    private SeckillOrderService orderService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @RabbitListener(queues = "seckill.order.queue")
    public void processSeckillOrder(Map<String, Object> message) {
        Long activityId = Long.valueOf(message.get("activityId").toString());
        Long userId = Long.valueOf(message.get("userId").toString());
        
        try {
            // 创建订单
            orderService.createSeckillOrder(activityId, userId);
            log.info("秒杀订单创建成功: activityId={}, userId={}", activityId, userId);
            
        } catch (Exception e) {
            log.error("创建秒杀订单失败", e);
            // 创建订单失败,恢复库存
            recoverStock(activityId, userId);
        }
    }
    
    /**
     * 恢复库存
     */
    private void recoverStock(Long activityId, Long userId) {
        String stockKey = RedisKeyManager.getStockKey(activityId);
        String userKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
        
        redisTemplate.opsForValue().increment(stockKey);
        redisTemplate.delete(userKey);
        
        log.warn("恢复库存: activityId={}, userId={}", activityId, userId);
    }
}
3.6 订单服务
bash 复制代码
@Service
@Transactional
@Slf4j
public class SeckillOrderService {
    
    @Autowired
    private SeckillOrderMapper orderMapper;
    
    @Autowired
    private SeckillActivityMapper activityMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 创建秒杀订单
     */
    public void createSeckillOrder(Long activityId, Long userId) {
        // 双重校验,防止消息重复消费
        String userKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
        if (!Boolean.TRUE.equals(redisTemplate.hasKey(userKey))) {
            log.warn("用户秒杀记录不存在: activityId={}, userId={}", activityId, userId);
            return;
        }
        
        // 查询活动信息
        SeckillActivity activity = getActivityFromCache(activityId);
        if (activity == null) {
            throw new RuntimeException("秒杀活动不存在");
        }
        
        // 生成订单号
        String orderNo = generateOrderNo();
        
        // 创建订单
        SeckillOrder order = new SeckillOrder();
        order.setUserId(userId);
        order.setActivityId(activityId);
        order.setProductId(activity.getProductId());
        order.setOrderNo(orderNo);
        order.setAmount(activity.getSeckillPrice());
        order.setQuantity(1);
        order.setStatus(0); // 待支付
        order.setCreateTime(new Date());
        
        orderMapper.insert(order);
        
        // 更新数据库库存(可选,用于对账)
        updateDatabaseStock(activityId);
        
        log.info("创建秒杀订单成功: orderNo={}, userId={}", orderNo, userId);
    }
    
    /**
     * 从缓存获取活动信息
     */
    private SeckillActivity getActivityFromCache(Long activityId) {
        String activityKey = RedisKeyManager.getActivityKey(activityId);
        Object activityObj = redisTemplate.opsForValue().get(activityKey);
        if (activityObj instanceof SeckillActivity) {
            return (SeckillActivity) activityObj;
        }
        return activityMapper.selectById(activityId);
    }
    
    /**
     * 更新数据库库存
     */
    private void updateDatabaseStock(Long activityId) {
        int rows = activityMapper.decreaseStock(activityId);
        if (rows == 0) {
            log.error("更新数据库库存失败: activityId={}", activityId);
            throw new RuntimeException("库存不足");
        }
    }
    
    /**
     * 生成订单号
     */
    private String generateOrderNo() {
        return "SO" + System.currentTimeMillis() + RandomUtil.randomNumbers(6);
    }
}
3.7 控制器层
bash 复制代码
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {
    
    @Autowired
    private SeckillService seckillService;
    
    @Autowired
    private SeckillPreheatService preheatService;
    
    /**
     * 秒杀接口
     */
    @PostMapping("/{activityId}")
    public SeckillResult seckill(@PathVariable Long activityId, 
                                @RequestHeader("userId") Long userId) {
        try {
            // 使用方案二:Lua脚本保证原子性
            return seckillService.seckillV2(activityId, userId);
        } catch (Exception e) {
            log.error("秒杀异常", e);
            return SeckillResult.error("系统繁忙,请重试");
        }
    }
    
    /**
     * 预热库存
     */
    @PostMapping("/preheat/{activityId}")
    public String preheat(@PathVariable Long activityId) {
        preheatService.preheatStock(activityId);
        return "预热成功";
    }
    
    /**
     * 查询库存
     */
    @GetMapping("/stock/{activityId}")
    public Integer getStock(@PathVariable Long activityId) {
        return preheatService.getStockFromRedis(activityId);
    }
}

// 返回结果封装
@Data
class SeckillResult {
    private boolean success;
    private String message;
    private String orderNo;
    
    public static SeckillResult success(String message) {
        SeckillResult result = new SeckillResult();
        result.setSuccess(true);
        result.setMessage(message);
        return result;
    }
    
    public static SeckillResult error(String message) {
        SeckillResult result = new SeckillResult();
        result.setSuccess(false);
        result.setMessage(message);
        return result;
    }
}

四、方案总结

  1. Redis原子操作

    bash 复制代码
    // 关键代码:原子扣减库存
    Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
  2. Lua脚本原子性

    • 将库存检查、扣减、用户记录等多个操作封装在一个Lua脚本中
    • Redis单线程执行,保证原子性
  3. 分布式锁

    bash 复制代码
    // 防止极端情况下的并发问题
    RLock lock = redissonClient.getLock(lockKey);
    lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
  4. 用户防重

    bash 复制代码
    // 记录用户参与记录
    String userSeckillKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
    redisTemplate.opsForValue().set(userSeckillKey, "1", Duration.ofMinutes(30));
  5. 异常恢复机制

    bash 复制代码
    // 下单失败时恢复库存
    redisTemplate.opsForValue().increment(stockKey);
    redisTemplate.delete(userKey);
相关推荐
程序猿DD4 小时前
如何在 Spring Boot 应用中配置多个 Spring AI 的 LLM 客户端
spring boot·llm·spring ai
Code blocks4 小时前
SpringBoot自定义请求前缀
java·spring boot·后端
爱学大树锯4 小时前
【Spring Boot JAR 解压修改配置后重新打包全流程(避坑指南)】
spring boot·后端·jar
kobe_OKOK_4 小时前
Django ORM 无法通过 `ForeignKey` 自动关联,而是需要 **根据父模型中的某个字段(比如 ID)去查询子模型**。
后端·python·django
Jabes.yang5 小时前
Java求职面试:从Spring Boot到Kafka的技术探讨
java·spring boot·面试·kafka·互联网大厂
程序员爱钓鱼5 小时前
Go语言实战案例——进阶与部署篇:编写Makefile自动构建Go项目
后端·算法·go
canonical_entropy6 小时前
DDD本质论:从哲学到数学,再到工程实践的完整指南之实践篇
java·后端·领域驱动设计
该用户已不存在6 小时前
别再用 if err != nil 了,学会这几个技巧,假装自己是Go大神
后端·go
6 小时前
从0开始搭建web应用
后端