基于 SpringBoot + Redis (Lettuce) + RabbitMQ 实现「Redis 预扣库存 + 异步同步数据库」

一、整体流程回顾

  1. 前端下单请求进来,先查 Redis 库存
  2. 执行 DECR 原子扣减:
    • 结果 < 0 → 库存不足,直接返回失败
    • 结果 ≥ 0 → 扣减成功,发送消息到 MQ
  3. MQ 消费者消费消息,异步更新数据库库存
  4. 配套:消息重试、定时对账、DB 乐观锁防重复扣减

二、核心依赖(pom.xml 关键)

复制代码
<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- MyBatis/MyBatis-Plus 操作DB -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

三、常量 & MQ 队列配置

1. 常量类

复制代码
public class StockConstant {
    // Redis 库存 key 前缀
    public static final String REDIS_STOCK_PREFIX = "product:stock:";
    // MQ 队列名称
    public static final String STOCK_SYNC_QUEUE = "stock.sync.queue";
}

2. RabbitMQ 配置(队列、交换机、绑定)

复制代码
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

    @Bean
    public Queue stockSyncQueue() {
        // 持久化队列,宕机不丢失消息
        return new Queue(StockConstant.STOCK_SYNC_QUEUE, true);
    }
}

四、库存实体 & Mapper(DB 层)

1. 商品库存实体(带乐观锁)

复制代码
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;

@Data
public class ProductStock {
    private Long id;
    // 商品ID
    private Long productId;
    // 数据库真实库存
    private Integer stock;
    // 乐观锁版本号,防止异步重复扣减
    @Version
    private Integer version;
}

2. Mapper 接口(乐观锁更新库存)

复制代码
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

public interface ProductStockMapper extends BaseMapper<ProductStock> {

    /**
     * 扣减数据库库存(乐观锁)
     * @param productId 商品ID
     * @param deductNum 扣减数量(这里秒杀固定为1)
     * @param version 版本号
     * @return 影响行数,0=更新失败(重复扣/版本过期)
     */
    int deductStock(@Param("productId") Long productId,
                    @Param("deductNum") Integer deductNum,
                    @Param("version") Integer version);
}

3. Mapper XML

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mapper.ProductStockMapper">

    <update id="deductStock">
        UPDATE product_stock
        SET stock = stock - #{deductNum}, version = version + 1
        WHERE product_id = #{productId}
          AND version = #{version}
          AND stock >= #{deductNum}
    </update>

</mapper>

五、下单接口:Redis 预扣库存 + 发 MQ(核心入口)

复制代码
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class StockService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 秒杀下单:Redis 原子预扣库存
     * @param productId 商品ID
     * @return true=扣减成功,false=库存不足
     */
    public boolean seckillDeductStock(Long productId) {
        String redisKey = StockConstant.REDIS_STOCK_PREFIX + productId;

        // 1. Redis DECR 原子扣减,单线程保证不会超卖
        Long remainStock = stringRedisTemplate.opsForValue().decrement(redisKey);

        // 2. 剩余库存 < 0,扣减失败,回滚+返回
        if (remainStock < 0) {
            // 把多扣的加回来
            stringRedisTemplate.opsForValue().increment(redisKey);
            return false;
        }

        // 3. 扣减成功,发送消息到MQ,异步同步DB
        StockSyncMsg msg = new StockSyncMsg();
        msg.setProductId(productId);
        msg.setDeductNum(1); // 单次秒杀扣1件
        rabbitTemplate.convertAndSend(StockConstant.STOCK_SYNC_QUEUE, msg);

        return true;
    }
}

消息实体(MQ 传输对象)

复制代码
import lombok.Data;
import java.io.Serializable;

@Data
public class StockSyncMsg implements Serializable {
    // 商品ID
    private Long productId;
    // 本次扣减数量
    private Integer deductNum;
}

六、MQ 消费者:异步同步数据库库存

开启消息重试、手动 ACK,保证最终一致性

复制代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;

@Component
public class StockSyncConsumer {

    @Resource
    private ProductStockMapper stockMapper;

    /**
     * 监听库存同步队列,异步更新DB
     * 手动ACK:失败不删除消息,自动重试
     */
    @RabbitListener(queues = StockConstant.STOCK_SYNC_QUEUE)
    public void consume(StockSyncMsg msg, Channel channel,
                        @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        Long productId = msg.getProductId();
        int deductNum = msg.getDeductNum();

        try {
            // 1. 查询当前DB库存+版本号
            ProductStock stock = stockMapper.selectById(productId);
            if (stock == null || stock.getStock() < deductNum) {
                // DB已无库存,拒绝消息,不再重试
                channel.basicNack(deliveryTag, false, false);
                return;
            }

            // 2. 乐观锁更新库存
            int rows = stockMapper.deductStock(productId, deductNum, stock.getVersion());
            if (rows > 0) {
                // 更新成功,手动ACK
                channel.basicAck(deliveryTag, false);
            } else {
                // 版本冲突/重复扣减,丢弃消息
                channel.basicNack(deliveryTag, false, false);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // 异常:拒绝并重回队列,等待重试
            channel.basicNack(deliveryTag, false, true);
        }
    }
}

七、前置初始化:秒杀前加载库存到 Redis

秒杀活动开始前,必须先把 DB 库存全量载入 Redis,否则 DECR 直接变成负数:

复制代码
@Service
public class StockInitService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ProductStockMapper stockMapper;

    /**
     * 秒杀预热:DB库存 → 全量加载到Redis
     */
    public void loadStockToRedis(Long productId) {
        ProductStock stock = stockMapper.selectById(productId);
        if (stock != null) {
            String key = StockConstant.REDIS_STOCK_PREFIX + productId;
            stringRedisTemplate.opsForValue().set(key, stock.getStock().toString());
        }
    }
}

八、兜底方案(代码思路,可直接实现)

1. 定时对账任务(解决 Redis/DB 数据不一致)

使用 @Scheduled 定时比对:

  1. 定时拉取所有参与秒杀商品

  2. 分别查询 Redis 库存、DB 库存

  3. 差值不为 0 → 以DB 为准修正 Redis 库存

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.util.List;

    @Slf4j
    @Component
    public class StockCheckTask {

    复制代码
     @Resource
     private StringRedisTemplate stringRedisTemplate;
    
     @Resource
     private ProductStockMapper productStockMapper;
    
     // Redis 库存 key 前缀(和你业务保持一致)
     private static final String REDIS_STOCK_PREFIX = "product:stock:";
    
     /**
      * 定时对账:每 5 分钟执行一次
      * 核心逻辑:以 DB 为最终权威,修正 Redis 库存
      */
     @Scheduled(fixedRate = 1000 * 60 * 5)
     public void stockReconciliation() {
         log.info("===== 开始执行 Redis <-> DB 库存定时对账 =====");
    
         try {
             // 1. 查询所有需要参与秒杀/库存对账的商品(你可以加条件:只查活动商品)
             List<ProductStock> productStockList = productStockMapper.selectList(null);
    
             for (ProductStock dbStock : productStockList) {
                 Long productId = dbStock.getProductId();
                 Integer realDbStock = dbStock.getStock(); // DB 真实库存(权威值)
    
                 // 2. 拼接 Redis key
                 String redisKey = REDIS_STOCK_PREFIX + productId;
    
                 // 3. 获取 Redis 当前库存
                 String redisStockStr = stringRedisTemplate.opsForValue().get(redisKey);
                 if (redisStockStr == null) {
                     log.warn("商品{} Redis 库存不存在,自动从DB同步", productId);
                     stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(realDbStock));
                     continue;
                 }
    
                 // 转成数字
                 int redisStock = Integer.parseInt(redisStockStr);
    
                 // 4. 对比:不一致 → 强制以 DB 为准修正 Redis
                 if (redisStock != realDbStock) {
                     log.warn("商品{} 库存不一致!DB={},Redis={},自动修正 Redis",
                             productId, realDbStock, redisStock);
    
                     // 修正 Redis = DB 真实库存(最终一致性兜底)
                     stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(realDbStock));
                 }
             }
    
             log.info("===== 库存对账执行完成 =====");
         } catch (Exception e) {
             log.error("===== 库存对账异常 =====", e);
         }
     }

    }

这个对账任务的核心作用

  1. Redis 扣多了 → 自动把 Redis 改成 DB 真实值
  2. Redis 丢数据 / 宕机恢复 → 自动补全库存
  3. MQ 消费失败、漏同步 → 最终能修复一致
  4. 绝对防止超卖 :永远以 DB 为权威数据源

必须开启的配置(application.yml)

复制代码
spring:
  application:
    name: seckill-demo
  # 开启定时任务
  task:
    scheduling:
      enabled: true

启动类必须加注解

复制代码
@SpringBootApplication
@EnableScheduling  // 必须加!
@EnableRabbit
public class SeckillApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class, args);
    }
}

2. 防超卖总结

  • Redis DECR 原子操作 + 单线程模型:拦截绝大部分超卖
  • DB 乐观锁:兜底防止异步重复扣减
  • 消息重试 + 定时对账:保证最终一致性

九、使用顺序(上线步骤)

  1. 秒杀预热:调用 loadStockToRedis() 将 DB 库存载入 Redis
  2. 前端请求 → 调用 seckillDeductStock() 预扣库存
  3. MQ 消费者自动异步更新 DB
  4. 定时任务自动对账修复数据偏差
相关推荐
mosaic_born2 小时前
centos 7.9 离线部署Zabbix 6.0.46 监控详细方案(解决数据库字符集问题)
数据库·centos·zabbix
weelinking2 小时前
【产品】10_搭建前端框架——把你的原型变成真实页面
java·大数据·前端·数据库·人工智能·python·前端框架
一 乐2 小时前
图书电子商务网站系统|基于SprinBoot+vue图书电子商务网站设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·图书电子商务网站系统
lzp07913 小时前
元数据驱动开发 - 面向对象编程思想的补充(上)
spring boot·后端·ui
●VON10 小时前
鸿蒙Flutter实战:分类管理页BottomSheet CRUD
数据库·flutter·华为·harmonyos·鸿蒙
Cosolar10 小时前
Chroma向量库面试学习指南
数据库·人工智能·面试·职场和发展·数据库架构
企服AI产品测评局11 小时前
Agent适配信创环境实测:企业级自动化如何实现国产操作系统与数据库全兼容?
运维·数据库·人工智能·ai·chatgpt·自动化
cfm_291411 小时前
Redis数据安全性解析
数据库·redis·缓存
DIY源码阁12 小时前
JavaSwing学生成绩管理系统 - MySQL版
java·数据库·mysql·eclipse