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

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

方案概述

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

  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. 异步处理:非核心链路采用异步处理
相关推荐
亦暖筑序3 小时前
Java 8老系统AI Workflow实战:把一次性AI对话升级成可恢复工作流
java·后端
敲代码的彭于晏4 小时前
Bean 生命周期完全图解:前端同学也能看懂的 Spring 核心机制
java·前端·后端
plainGeekDev5 小时前
ButterKnife → ViewBinding
android·java·kotlin
像我这样帅的人丶你还21 小时前
Java 后端详解(四):分页与搜索
java·javascript·后端
她的男孩21 小时前
数据权限为什么不能只靠注解?Forge 的 Mapper 层 SQL 改写源码拆解
java·后端·架构
tntxia1 天前
Mybatis的日志输入
java
亦暖筑序1 天前
Java 8老系统Browser Agent实战:三层拦截把AI操作后台变成可审计流程
java·后端·设计模式
用户298698530141 天前
Java 实现 Word 文档加密与权限解除
java·后端
Yeats_Liao1 天前
14:Servlet中的页面跳转-Java Web
java·后端·架构
未秃头的程序猿1 天前
告别"if-else地狱"!Java 21模式匹配,代码优雅了10倍
java·后端·面试