库存管理系统的数据库设计:从进销存到分布式库存的一体化方案
库存管理系统的数据库设计从进销存到分布式库存的一体化方案一、库存显示有货仓库却说发不出库存数据不一致的根源某电商仓库凌晨2点收到紧急告警系统显示某SKU还有523件库存但实际盘点只剩7件。追查发现过去72小时内有3次退货入库操作在数据库主从切换时丢失——主库在提交事务后宕机从库提升为主库时选择了到主库宕机前那一刻的数据退货记录丢失了但Redis缓存中的库存扣减记录还在。库存数不一致有三个经典来源数据库主从延迟主库入库写入从库还有2秒延迟查询走从库看到的是旧数据Redis与MySQL不一致高性能的库存扣减走Redis异步同步到MySQL时有丢失风险分布式事务的影子库存跨仓库调拨时调出仓库已经扣减调入仓库还未增加——中间态的库存暂时消失了二、库存系统的三阶段架构演进三、RedisMySQL双写的库存实现Service public class InventoryService { private final RedisTemplateString, Integer redis; private final JdbcTemplate mysql; private final KafkaTemplateString, InventoryEvent kafka; // Redis库存Key前缀 private static final String STOCK_KEY inventory:stock:; // Lua脚本原子性扣减库存 private static final String DEDUCT_SCRIPT local key KEYS[1] local quantity tonumber(ARGV[1]) local current tonumber(redis.call(GET, key) or 0) if current quantity then redis.call(DECRBY, key, quantity) return 1 else return 0 end ; private final RedisScriptLong deductScript; public boolean deductStock(String skuId, String warehouseId, int quantity) { String redisKey STOCK_KEY skuId : warehouseId; try { // Step 1: Redis原子扣减 Long result redis.execute( deductScript, Collections.singletonList(redisKey), String.valueOf(quantity) ); if (result ! 1) { return false; // 库存不足 } // Step 2: 异步写MySQL 发送事件 asyncExecutor.submit(() - { try { mysql.update( UPDATE inventory SET stock stock - ?, version version 1 WHERE sku_id ? AND warehouse_id ? AND stock ?, quantity, skuId, warehouseId, quantity ); // 发送库存变更事件 kafka.send(inventory_change, new InventoryEvent(skuId, warehouseId, -quantity) ); } catch (Exception e) { // MySQL写入失败 → 补偿回Redis redis.opsForValue().increment(redisKey, quantity); // 写入对账队列 reconciliationQueue.add(skuId, warehouseId, quantity); throw new InventoryException(库存MySQL同步失败, e); } }); return true; } catch (RedisException e) { // Redis不可用 → 降级到MySQL直接扣减 return deductStockFromMySQL(skuId, warehouseId, quantity); } } private boolean deductStockFromMySQL(String skuId, String warehouseId, int quantity) { try { int rows mysql.update( UPDATE inventory SET stock stock - ? WHERE sku_id ? AND warehouse_id ? AND stock ?, quantity, skuId, warehouseId, quantity ); return rows 0; } catch (Exception e) { throw new InventoryException(库存MySQL扣减失败, e); } } // 定时对账任务每小时比对Redis和MySQL的库存数 Scheduled(fixedDelay 3600000) public void reconcileInventory() { ListInventoryRecon diffs mysql.query( SELECT sku_id, warehouse_id, stock AS mysql_stock FROM inventory, (rs, rowNum) - new InventoryRecon( rs.getString(sku_id), rs.getString(warehouse_id), rs.getInt(mysql_stock) ) ); for (InventoryRecon recon : diffs) { String redisKey STOCK_KEY recon.skuId : recon.warehouseId; Integer redisStock redis.opsForValue().get(redisKey); if (redisStock ! null redisStock ! recon.mysqlStock) { // 差异 1% 时触发告警 double diffPct Math.abs(redisStock - recon.mysqlStock) / (double) Math.max(redisStock, recon.mysqlStock); if (diffPct 0.01) { alertInventoryDifference(recon, redisStock); // 以MySQL为准修正Redis redis.opsForValue().set(redisKey, recon.mysqlStock); } } } } }四、分布式库存的三个工程难题难题一TCC分布式事务的空回滚。仓库A→仓库B调拨100件Try阶段A扣减100预留B未操作。Cancel阶段如果A收到Cancel请求——但A的Try根本没执行过消息乱序A应该返回成功而非报错。这就是TCC的空回滚保障。难题二库存对账的时间窗口。Redis扣减发生在11:00:00MySQL同步在11:00:05。如果在11:00:03做了对账会误报差异。对账必须以MySQL的更新时间戳为准忽略最近5秒内的变更。难题三预售/秒杀的超卖。秒杀场景中Redis的Lua脚本只能保证Redis层面的原子性无法保证10台Redis实例之间的全局一致性。真正的防超卖方案需要Redis集群级别的分布式锁或使用Redisson的RAtomicLong做集群扣减。五、总结库存管理的数据库设计需要分层靠谱Redis做高性能的读写层99%的库存操作在Redis完成MySQL做持久化和审计层保证数据不丢失Kafka做事件溯源数据库挂了可以从事件流重建库存快照。最容易被忽视的是对账机制——Redis和MySQL的不一致是常态网络延迟、异步写入、主从切换关键不是消除不一致而是快速检测并修复不一致。本文属于「行业场景与项目复盘」系列深度分析库存管理系统的RedisMySQL双写架构与分布式一致性保障。