电商平台库存扣减方案设计

电商平台库存扣减方案设计

方案概述

本方案针对电商平台商品库存扣减或抽奖活动奖品库存扣减场景,设计了一套完整的分布式库存管理方案。核心思路是:

  1. 流量过滤:参与前校验,过滤恶意流量
  2. 缓存预热:库存数据预加载到Redis
  3. 原子扣减:使用Redis DECR实现原子性扣减
  4. 分布式锁:SETNX加锁防止Redis崩溃导致超卖
  5. 异步同步:延时队列 + 分批扣减实现最终一致性

核心流程设计

1. 整体流程图

数据库 定时任务 延时队列 分布式锁服务 Redis缓存 流量校验服务 用户请求 数据库 定时任务 延时队列 分布式锁服务 Redis缓存 流量校验服务 用户请求 alt [扣减结果 > 0] [扣减结果 <= 0] alt [校验不通过] [校验通过] loop [延时任务] 发起库存扣减请求 参与前校验 返回失败 DECR原子扣减库存 扣减成功 SETNX加锁(防崩溃) 加锁成功 发送异步同步任务 入队成功 返回成功 库存不足 返回库存不足 触发同步任务 分批从队列获取 批量更新数据库库存 更新成功

2. 方案优势分析

优势 说明
高性能 Redis操作内存,性能远超数据库
原子性 DECR原子操作,防止并发超卖
高可用 分布式锁防止Redis崩溃导致超卖
流量平滑 延时队列+分批扣减,平滑数据库压力
最终一致性 异步同步保证数据最终一致

详细实现设计

1. 库存预热机制

java 复制代码
/**
 * 库存预热服务
 */
@Service
@Slf4j
public class StockWarmUpService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private IStockDao stockDao;
    
    /**
     * 预热库存到Redis
     * @param productId 商品ID
     */
    public void warmUpStock(String productId) {
        // 1. 从数据库查询库存
        Stock stock = stockDao.getStockByProductId(productId);
        
        // 2. 设置库存到Redis
        String stockKey = buildStockKey(productId);
        redisTemplate.opsForValue().set(stockKey, stock.getQuantity());
        
        // 3. 设置库存锁key
        String lockKey = buildLockKey(productId);
        redisTemplate.opsForValue().setIfAbsent(lockKey, "0", Duration.ofHours(24));
        
        log.info("库存预热完成, productId: {}, stock: {}", productId, stock.getQuantity());
    }
    
    /**
     * 批量预热库存
     */
    @PostConstruct
    public void batchWarmUp() {
        // 1. 获取所有需要预热的商品
        List<String> productIds = stockDao.getAllActiveProductIds();
        
        // 2. 分批预热,每批100个
        List<List<String>> batches = Lists.partition(productIds, 100);
        
        for (List<String> batch : batches) {
            // 并行预热
            batch.parallelStream().forEach(this::warmUpStock);
            
            // 批次间延迟,避免Redis压力过大
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    private String buildStockKey(String productId) {
        return String.format("stock:%s:quantity", productId);
    }
    
    private String buildLockKey(String productId) {
        return String.format("stock:%s:lock", productId);
    }
}

2. 库存扣减服务

java 复制代码
/**
 * 库存扣减服务
 */
@Service
@Slf4j
public class StockDeductionService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private StockSyncQueue stockSyncQueue;
    
    @Autowired
    private DistributedLockService distributedLockService;
    
    @Autowired
    private StockValidator stockValidator;
    
    /**
     * 扣减库存
     * @param productId 商品ID
     * @param userId 用户ID
     * @param quantity 扣减数量
     * @return 扣减结果
     */
    public DeductionResult deduct(String productId, String userId, int quantity) {
        // 1. 参与前校验
        ValidationResult validation = stockValidator.validate(productId, userId, quantity);
        if (!validation.isPassed()) {
            log.warn("库存扣减校验不通过, productId: {}, userId: {}, reason: {}", 
                    productId, userId, validation.getReason());
            return DeductionResult.fail(validation.getReason());
        }
        
        String stockKey = buildStockKey(productId);
        String lockKey = buildLockKey(productId);
        
        // 2. Redis原子扣减
        Long remainingStock = redisTemplate.opsForValue().decrement(stockKey, quantity);
        
        if (remainingStock == null || remainingStock < 0) {
            // 扣减失败,恢复库存
            redisTemplate.opsForValue().increment(stockKey, quantity);
            log.warn("库存不足, productId: {}, remainingStock: {}", productId, remainingStock);
            return DeductionResult.fail("库存不足");
        }
        
        try {
            // 3. 加分布式锁,防止Redis崩溃导致超卖
            boolean lockAcquired = distributedLockService.tryLock(lockKey, 10, TimeUnit.SECONDS);
            
            if (!lockAcquired) {
                // 加锁失败,回滚库存
                redisTemplate.opsForValue().increment(stockKey, quantity);
                log.error("获取分布式锁失败, productId: {}", productId);
                return DeductionResult.fail("系统繁忙,请稍后重试");
            }
            
            // 4. 发送异步同步任务
            StockSyncTask syncTask = StockSyncTask.builder()
                    .productId(productId)
                    .userId(userId)
                    .quantity(quantity)
                    .deductTime(LocalDateTime.now())
                    .build();
            
            stockSyncQueue.send(syncTask);
            
            log.info("库存扣减成功, productId: {}, userId: {}, quantity: {}, remainingStock: {}", 
                    productId, userId, quantity, remainingStock);
            
            return DeductionResult.success(productId, quantity, remainingStock);
            
        } finally {
            // 释放锁
            distributedLockService.unlock(lockKey);
        }
    }
    
    private String buildStockKey(String productId) {
        return String.format("stock:%s:quantity", productId);
    }
    
    private String buildLockKey(String productId) {
        return String.format("stock:%s:lock", productId);
    }
}

3. 流量校验服务

java 复制代码
/**
 * 库存扣减校验服务
 */
@Service
@Slf4j
public class StockValidator {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private BlacklistService blacklistService;
    
    @Autowired
    private UserActivityService userActivityService;
    
    /**
     * 参与前校验
     */
    public ValidationResult validate(String productId, String userId, int quantity) {
        // 1. 黑名单校验
        if (blacklistService.isBlacklisted(userId)) {
            return ValidationResult.fail("用户已被加入黑名单");
        }
        
        // 2. 用户频率校验
        if (!checkUserFrequency(userId)) {
            return ValidationResult.fail("操作过于频繁,请稍后重试");
        }
        
        // 3. 库存预检查
        if (!checkStock(productId, quantity)) {
            return ValidationResult.fail("库存不足");
        }
        
        // 4. 用户参与次数校验
        if (!checkUserParticipation(userId, productId)) {
            return ValidationResult.fail("已达到参与上限");
        }
        
        return ValidationResult.pass();
    }
    
    /**
     * 检查用户频率
     */
    private boolean checkUserFrequency(String userId) {
        String freqKey = String.format("user:%s:frequency", userId);
        Long count = redisTemplate.opsForValue().increment(freqKey);
        
        if (count == null) {
            return false;
        }
        
        // 设置过期时间,窗口期为1分钟
        if (count == 1) {
            redisTemplate.expire(freqKey, 60, TimeUnit.SECONDS);
        }
        
        // 限制每分钟最多请求10次
        return count <= 10;
    }
    
    /**
     * 检查库存
     */
    private boolean checkStock(String productId, int quantity) {
        String stockKey = buildStockKey(productId);
        String stockStr = redisTemplate.opsForValue().get(stockKey);
        
        if (StringUtils.isEmpty(stockStr)) {
            return false;
        }
        
        int stock = Integer.parseInt(stockStr);
        return stock >= quantity;
    }
    
    /**
     * 检查用户参与次数
     */
    private boolean checkUserParticipation(String userId, String productId) {
        String participationKey = String.format("user:%s:participation:%s", userId, productId);
        String countStr = redisTemplate.opsForValue().get(participationKey);
        
        int count = StringUtils.isEmpty(countStr) ? 0 : Integer.parseInt(countStr);
        int maxParticipation = 3; // 最多参与3次
        
        return count < maxParticipation;
    }
    
    private String buildStockKey(String productId) {
        return String.format("stock:%s:quantity", productId);
    }
}

4. 延时队列设计

java 复制代码
/**
 * 库存同步延时队列
 */
@Component
@Slf4j
public class StockSyncQueue {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String SYNC_QUEUE_KEY = "stock:sync:queue";
    private static final String SYNC_DELAYED_KEY = "stock:sync:delayed";
    
    /**
     * 发送同步任务
     */
    public void send(StockSyncTask task) {
        // 1. 任务延迟5秒执行
        long delayTime = System.currentTimeMillis() + 5000;
        
        // 2. 任务数据序列化
        String taskData = JSON.toJSONString(task);
        
        // 3. 添加到延时队列(使用ZSET实现延时队列)
        redisTemplate.opsForZSet().add(SYNC_DELAYED_KEY, taskData, delayTime);
        
        log.info("同步任务已发送, productId: {}", task.getProductId());
    }
    
    /**
     * 获取待执行任务
     */
    public List<StockSyncTask> getExecutableTasks() {
        long now = System.currentTimeMillis();
        
        // 1. 获取已到执行时间的任务
        Set<String> taskDataSet = redisTemplate.opsForZSet()
                .rangeByScore(SYNC_DELAYED_KEY, 0, now);
        
        if (CollectionUtils.isEmpty(taskDataSet)) {
            return Collections.emptyList();
        }
        
        List<StockSyncTask> tasks = new ArrayList<>();
        
        for (String taskData : taskDataSet) {
            try {
                StockSyncTask task = JSON.parseObject(taskData, StockSyncTask.class);
                tasks.add(task);
            } catch (Exception e) {
                log.error("任务数据解析失败: {}", taskData, e);
            }
        }
        
        return tasks;
    }
    
    /**
     * 确认任务完成
     */
    public void confirmTask(StockSyncTask task) {
        String taskData = JSON.toJSONString(task);
        redisTemplate.opsForZSet().remove(SYNC_DELAYED_KEY, taskData);
    }
    
    /**
     * 任务执行失败,重试
     */
    public void retryTask(StockSyncTask task) {
        // 延迟10秒后重试
        long delayTime = System.currentTimeMillis() + 10000;
        String taskData = JSON.toJSONString(task);
        redisTemplate.opsForZSet().add(SYNC_DELAYED_KEY, taskData, delayTime);
    }
}

5. 定时任务实现

java 复制代码
/**
 * 库存同步定时任务
 */
@Component
@Slf4j
public class StockSyncTaskJob {
    
    @Autowired
    private StockSyncQueue stockSyncQueue;
    
    @Autowired
    private IStockDao stockDao;
    
    @Autowired
    private StockSyncRecordService syncRecordService;
    
    /**
     * 有界阻塞队列
     */
    private final BlockingQueue<StockSyncTask> blockingQueue = 
            new ArrayBlockingQueue<>(10000);
    
    /**
     * 分批处理数据库更新
     * 每5秒执行一次
     */
    @Scheduled(fixedDelay = 5000)
    public void execute() {
        log.info("开始执行库存同步任务");
        
        try {
            // 1. 从延时队列获取待执行任务
            List<StockSyncTask> tasks = stockSyncQueue.getExecutableTasks();
            
            if (CollectionUtils.isEmpty(tasks)) {
                return;
            }
            
            // 2. 添加到有界阻塞队列
            for (StockSyncTask task : tasks) {
                if (!blockingQueue.offer(task)) {
                    // 队列满时,优先处理队列中的任务
                    processBatch();
                    blockingQueue.offer(task);
                }
            }
            
            // 3. 分批处理
            processBatch();
            
        } catch (Exception e) {
            log.error("库存同步任务执行失败", e);
        }
    }
    
    /**
     * 批量处理
     */
    private void processBatch() {
        List<StockSyncTask> batch = new ArrayList<>();
        
        // 从队列中取出一批任务
        blockingQueue.drainTo(batch, 100);
        
        if (batch.isEmpty()) {
            return;
        }
        
        try {
            // 4. 分批更新数据库
            stockDao.batchDeductStock(batch);
            
            // 5. 记录同步记录
            syncRecordService.saveSyncRecords(batch);
            
            // 6. 确认任务完成
            for (StockSyncTask task : batch) {
                stockSyncQueue.confirmTask(task);
            }
            
            log.info("批量库存同步完成, count: {}", batch.size());
            
        } catch (Exception e) {
            log.error("批量库存同步失败", e);
            
            // 失败的任务重试
            for (StockSyncTask task : batch) {
                stockSyncQueue.retryTask(task);
            }
        }
    }
}

6. 分布式锁服务

java 复制代码
/**
 * 分布式锁服务
 */
@Service
@Slf4j
public class DistributedLockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LOCK_PREFIX = "lock:";
    
    /**
     * 尝试获取锁
     * @param lockKey 锁的key
     * @param timeout 过期时间
     * @param unit 时间单位
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, long timeout, TimeUnit unit) {
        String key = LOCK_PREFIX + lockKey;
        String lockValue = UUID.randomUUID().toString();
        
        // SETNX + 设置过期时间(原子操作)
        Boolean result = redisTemplate.opsForValue()
                .setIfAbsent(key, lockValue, timeout, unit);
        
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放锁
     * @param lockKey 锁的key
     */
    public void unlock(String lockKey) {
        String key = LOCK_PREFIX + lockKey;
        redisTemplate.delete(key);
    }
    
    /**
     * 带心跳的锁续期
     */
    public boolean tryLockWithHeartbeat(String lockKey, long timeout, TimeUnit unit) {
        String key = LOCK_PREFIX + lockKey;
        String lockValue = UUID.randomUUID().toString();
        
        Boolean acquired = redisTemplate.opsForValue()
                .setIfAbsent(key, lockValue, timeout, unit);
        
        if (Boolean.TRUE.equals(acquired)) {
            // 启动续期线程
            startHeartbeat(key, lockValue, timeout, unit);
            return true;
        }
        
        return false;
    }
    
    /**
     * 启动心跳
     */
    private void startHeartbeat(String key, String lockValue, long timeout, TimeUnit unit) {
        // 续期线程
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        
        // 每30秒续期一次
        long period = unit.toSeconds(timeout) / 2;
        
        scheduler.scheduleAtFixedRate(() -> {
            try {
                // 检查锁是否还存在
                String currentValue = redisTemplate.opsForValue().get(key);
                if (lockValue.equals(currentValue)) {
                    // 续期
                    redisTemplate.expire(key, timeout, unit);
                }
            } catch (Exception e) {
                log.error("锁续期失败", e);
            }
        }, period, period, TimeUnit.SECONDS);
    }
}

7. 数据访问层

java 复制代码
/**
 * 库存DAO接口
 */
public interface IStockDao {
    
    /**
     * 获取库存
     */
    Stock getStockByProductId(String productId);
    
    /**
     * 扣减库存
     */
    int deductStock(String productId, int quantity);
    
    /**
     * 批量扣减库存
     */
    void batchDeductStock(List<StockSyncTask> tasks);
}

/**
 * 库存DAO实现
 */
@Repository
@Slf4j
public class StockDaoImpl implements IStockDao {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Override
    @Transactional
    public int deductStock(String productId, int quantity) {
        String sql = "UPDATE stock SET quantity = quantity - ? WHERE product_id = ? AND quantity >= ?";
        
        int rows = jdbcTemplate.update(sql, quantity, productId, quantity);
        
        if (rows > 0) {
            log.info("数据库库存扣减成功, productId: {}, quantity: {}", productId, quantity);
        } else {
            log.warn("数据库库存扣减失败, productId: {}, quantity: {}", productId, quantity);
        }
        
        return rows;
    }
    
    @Override
    @Transactional
    public void batchDeductStock(List<StockSyncTask> tasks) {
        // 分批更新,每批100条
        List<List<StockSyncTask>> batches = Lists.partition(tasks, 100);
        
        for (List<StockSyncTask> batch : batches) {
            batchDeductStockInBatch(batch);
        }
    }
    
    /**
     * 批量扣减库存(单次事务)
     */
    private void batchDeductStockInBatch(List<StockSyncTask> tasks) {
        String sql = "UPDATE stock SET quantity = quantity - ? WHERE product_id = ? AND quantity >= ?";
        
        jdbcTemplate.batchUpdate(sql, tasks.stream()
                .map(task -> Arrays.asList(task.getQuantity(), task.getProductId(), task.getQuantity()))
                .collect(Collectors.toList()));
        
        log.info("批量数据库库存扣减完成, count: {}", tasks.size());
    }
}

方案完整流程图









用户发起请求
流量校验
校验通过?
返回失败
Redis DECR扣减
扣减结果 > 0?
恢复库存,返回失败
SETNX加锁
加锁成功?
恢复库存,返回失败
发送延时队列
返回成功
延时队列触发
定时任务消费
有界队列缓冲
队列满?
立即处理
定时触发
分批更新数据库
更新成功
确认任务完成
更新失败
任务重试

关键指标监控

监控指标

指标 告警阈值 说明
库存扣减成功率 < 99.9% 监控扣减成功率
库存同步延迟 > 10秒 监控同步延迟时间
分布式锁获取失败率 > 1% 监控锁竞争情况
数据库更新失败率 > 0.1% 监控数据库更新情况
队列堆积数量 > 5000 监控队列堆积情况

监控代码

java 复制代码
/**
 * 库存监控服务
 */
@Service
@Slf4j
public class StockMonitorService {
    
    @Autowired
    private StockSyncQueue stockSyncQueue;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 获取监控指标
     */
    public StockMetrics getMetrics() {
        StockMetrics metrics = new StockMetrics();
        
        // 1. 队列堆积数量
        Long queueSize = redisTemplate.opsForZSet().zCard(StockSyncQueue.SYNC_DELAYED_KEY);
        metrics.setQueueSize(queueSize != null ? queueSize : 0);
        
        // 2. 库存扣减成功率(从Redis获取)
        String successKey = "stock:metrics:success";
        String failKey = "stock:metrics:fail";
        Long successCount = redisTemplate.opsForValue().increment(successKey);
        Long failCount = redisTemplate.opsForValue().increment(failKey);
        
        if (successCount != null && failCount != null && successCount > 0) {
            double successRate = (double) successCount / (successCount + failCount);
            metrics.setSuccessRate(successRate);
        }
        
        // 3. 分布式锁获取失败率
        String lockFailKey = "stock:metrics:lockFail";
        Long lockFailCount = redisTemplate.opsForValue().increment(lockFailKey);
        if (lockFailCount != null && lockFailCount > 0) {
            metrics.setLockFailRate(lockFailCount / (double) (successCount + lockFailCount));
        }
        
        return metrics;
    }
    
    /**
     * 告警检查
     */
    @Scheduled(fixedRate = 60000) // 每分钟检查一次
    public void checkAlerts() {
        StockMetrics metrics = getMetrics();
        
        if (metrics.getQueueSize() > 5000) {
            // 队列堆积告警
            alertService.sendAlert("库存同步队列堆积, 数量: " + metrics.getQueueSize());
        }
        
        if (metrics.getSuccessRate() < 0.999) {
            // 成功率告警
            alertService.sendAlert("库存扣减成功率低: " + metrics.getSuccessRate());
        }
    }
}

方案总结

核心优势

  1. 高性能:Redis DECR原子操作,单机可达10万+ QPS
  2. 高可靠:分布式锁防止Redis崩溃导致超卖
  3. 流量平滑:延时队列+分批处理,平滑数据库压力
  4. 最终一致性:异步同步保证数据最终一致
  5. 可扩展:支持水平扩展,应对业务增长

适用场景

  • 电商平台商品库存扣减
  • 抽奖活动奖品库存扣减
  • 优惠券发放数量控制
  • 限时限购活动

注意事项

  1. 库存预热:系统启动时需要预热库存到Redis
  2. 数据补偿:需要定期检查Redis和数据库的一致性
  3. 锁粒度:锁的粒度需要合理控制,避免影响性能
  4. 超时设置:分布式锁和延时队列的超时时间需要合理设置
  5. 重试策略:失败任务需要有合理的重试策略

性能优化建议

  1. 批量操作:数据库更新使用批量操作
  2. 连接池优化:合理配置Redis和数据库连接池
  3. 缓存预热:系统启动时预热热点数据
  4. 读写分离:数据库采用读写分离架构
  5. 异步处理:非核心链路采用异步处理
相关推荐
她说..6 小时前
Spring 核心工具类 AopUtils 超详细全解
java·后端·spring·springboot·spring aop
TH_16 小时前
33、IDEA无法获取最新分支
java·ide·intellij-idea
极客先躯7 小时前
Java Agent 技术全解析:从基础框架到落地实践
java·开发语言
yaso_zhang7 小时前
linux 下sudo运行程序,链接找不到问题处理
java·linux·服务器
帅气的你7 小时前
终于解决了!Spring Boot 启动慢的 5 个优化点
java
Croa-vo7 小时前
Optiver OA 气球节模拟题:拆解系统建模的核心逻辑,附避坑指南
java·数据结构·算法·leetcode·职场和发展
悟能不能悟7 小时前
Java CheckFailedException会去获取message.properties的内容吗
java·开发语言
shang_xs7 小时前
Java 25 ScopedValue - 作用域内安全访问的一种实现
java·开发语言·安全
小途软件7 小时前
基于深度学习的驾驶人情绪识别
java·人工智能·pytorch·python·深度学习·语言模型
小白学大数据7 小时前
Java 异步爬虫高效获取小红书短视频内容
java·开发语言·爬虫·python·音视频