电商平台库存扣减方案设计
方案概述
本方案针对电商平台商品库存扣减或抽奖活动奖品库存扣减场景,设计了一套完整的分布式库存管理方案。核心思路是:
- 流量过滤:参与前校验,过滤恶意流量
- 缓存预热:库存数据预加载到Redis
- 原子扣减:使用Redis DECR实现原子性扣减
- 分布式锁:SETNX加锁防止Redis崩溃导致超卖
- 异步同步:延时队列 + 分批扣减实现最终一致性
核心流程设计
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());
}
}
}
方案总结
核心优势
- 高性能:Redis DECR原子操作,单机可达10万+ QPS
- 高可靠:分布式锁防止Redis崩溃导致超卖
- 流量平滑:延时队列+分批处理,平滑数据库压力
- 最终一致性:异步同步保证数据最终一致
- 可扩展:支持水平扩展,应对业务增长
适用场景
- 电商平台商品库存扣减
- 抽奖活动奖品库存扣减
- 优惠券发放数量控制
- 限时限购活动
注意事项
- 库存预热:系统启动时需要预热库存到Redis
- 数据补偿:需要定期检查Redis和数据库的一致性
- 锁粒度:锁的粒度需要合理控制,避免影响性能
- 超时设置:分布式锁和延时队列的超时时间需要合理设置
- 重试策略:失败任务需要有合理的重试策略
性能优化建议
- 批量操作:数据库更新使用批量操作
- 连接池优化:合理配置Redis和数据库连接池
- 缓存预热:系统启动时预热热点数据
- 读写分离:数据库采用读写分离架构
- 异步处理:非核心链路采用异步处理