4. 库存核心表结构:商品表、仓库表、库存表、库存流水表的设计
好,咱们直接进入正题。库存系统最核心的,就是这四张表。我见过不少新手,一上来就想着怎么把库存算得特别准,结果表结构设计得一塌糊涂,后面改都改不动。说白了,表结构就是地基,地基歪了,楼盖得再高也得塌。
4.1 商品表(product)—— 一切业务的起点
商品表,记录的是「什么东西」。注意,它不记录数量,只记录属性。我在项目中遇到过有人把库存数量直接塞到商品表里,结果每次并发扣减都锁行,性能惨不忍睹。嗯,这是个大坑。
商品表的核心字段,我习惯这样设计:
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`sku_code` varchar(64) NOT NULL COMMENT 'SKU编码,唯一',
`product_name` varchar(200) NOT NULL COMMENT '商品名称',
`category_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
`unit` varchar(20) DEFAULT '件' COMMENT '计量单位',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1启用 0停用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku` (`sku_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
4.2 仓库表(warehouse)—— 库存的物理位置
仓库表,记录的是「东西放在哪」。别小看这张表,多仓库场景下,它的设计直接决定了库存查询的复杂度。
我个人习惯给仓库表加一个「仓库类型」字段,区分实体仓和虚拟仓。为什么?因为有些场景下,比如预售商品,它其实没有物理库存,但你需要一个虚拟仓位来记录。
CREATE TABLE `warehouse` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '仓库ID',
`warehouse_code` varchar(64) NOT NULL COMMENT '仓库编码',
`warehouse_name` varchar(200) NOT NULL COMMENT '仓库名称',
`warehouse_type` tinyint(4) DEFAULT '1' COMMENT '类型:1实体仓 2虚拟仓',
`address` varchar(500) DEFAULT NULL COMMENT '仓库地址',
`status` tinyint(4) DEFAULT '1' COMMENT '状态',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`warehouse_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库表';
4.3 库存表(inventory)—— 真正的核心
库存表,记录的是「某个商品在某个仓库有多少」。这是整个库存系统的核心,也是并发压力最大的地方。
库存表的设计,我踩过最大的坑就是「库存字段设计得太少」。一开始只设计了「可用库存」和「锁定库存」,结果后来业务方要求统计「在途库存」「次品库存」「预留库存」,改表改到想哭。
所以,我现在的建议是:
CREATE TABLE `inventory` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_id` bigint(20) NOT NULL COMMENT '商品ID',
`warehouse_id` bigint(20) NOT NULL COMMENT '仓库ID',
`available_qty` int(11) DEFAULT '0' COMMENT '可用库存',
`locked_qty` int(11) DEFAULT '0' COMMENT '锁定库存(已下单未出库)',
`onway_qty` int(11) DEFAULT '0' COMMENT '在途库存',
`defective_qty` int(11) DEFAULT '0' COMMENT '次品库存',
`version` int(11) DEFAULT '0' COMMENT '乐观锁版本号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_warehouse` (`product_id`, `warehouse_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
- 用「商品ID+仓库ID」作为联合唯一索引,保证一个商品在一个仓库只有一条记录
- 加乐观锁版本号,用于高并发下的库存扣减
- 库存字段全部用 int,不要用 decimal。为什么?因为库存是离散的整数,用 decimal 反而容易出精度问题
4.4 库存流水表(inventory_log)—— 审计的命脉
库存流水表,记录的是「库存为什么变了」。这张表平时看起来没啥用,但一旦出问题,它就是救命稻草。
我曾经遇到过一个线上事故:库存莫名其妙少了100件,业务方急得跳脚。幸好流水表设计得够详细,一查发现是某个定时任务重复执行了两次。如果没有流水表,这个锅可能就甩给数据库了。
CREATE TABLE `inventory_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_id` bigint(20) NOT NULL COMMENT '商品ID',
`warehouse_id` bigint(20) NOT NULL COMMENT '仓库ID',
`change_type` tinyint(4) NOT NULL COMMENT '变动类型:1入库 2出库 3锁定 4解锁 5盘盈 6盘亏',
`change_qty` int(11) NOT NULL COMMENT '变动数量(正数增加,负数减少)',
`before_qty` int(11) NOT NULL COMMENT '变动前可用库存',
`after_qty` int(11) NOT NULL COMMENT '变动后可用库存',
`biz_no` varchar(100) DEFAULT NULL COMMENT '业务单号(如订单号、采购单号)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`operator` varchar(50) DEFAULT NULL COMMENT '操作人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_product_warehouse` (`product_id`, `warehouse_id`),
KEY `idx_biz_no` (`biz_no`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';
- 记录变动前后的库存值,方便对账
- 关联业务单号,出了问题能追溯到源头
- 流水表只增不删,用时间索引来查询
4.5 四张表的关系与核心流程
这四张表的关系,我用一张图来展示:
这张图你看懂了吗?商品表和仓库表是「主数据」,库存表是它们俩的「交集」,而流水表则是库存表的「操作日志」。每次库存变动,必须先写流水,再更新库存。这个顺序不能乱,否则一旦出问题,你连回滚的依据都没有。
4.6 避坑指南
- 库存字段用 decimal 而不是 int —— 库存是离散的,不是连续的。用 decimal 不仅浪费空间,还容易在计算时出现精度问题。
- 流水表没有关联业务单号 —— 出了问题你只能看到库存变了,但不知道是谁变的。查起来就像大海捞针。
- 没有乐观锁版本号 —— 高并发下库存扣减会出现超卖。我见过一个电商项目,双十一当天因为没有乐观锁,超卖了3000单,赔了十几万。
- 商品表和库存表混在一起 —— 这是最基础的设计错误。商品属性(名称、分类)和库存数量是两回事,分开才能灵活扩展。
好了,这四张表的设计思路就这些。记住一句话:库存系统的核心不是「算得准」,而是「每一笔变动都有据可查」。把流水表设计好了,你就成功了一半。