库存数据模型设计:核心库存表结构设计、字段规范与约束、主键与唯一索引策略
做市系统的库存数据,说白了就是你的「家底」。家底都算不清楚,还做什么市?
我见过不少团队,一上来就追求高并发、低延迟,结果库存数据对不上,最后全盘重算。嗯,那场面,真是惨不忍睹。今天咱们就聊聊,怎么把库存表设计得既稳又准。
核心库存表结构设计
我个人习惯,库存表一定要分成两层:实时库存表和快照库存表。为什么?你想想看,实时库存要扛住每秒几千次的读写,快照库存要保证历史可追溯。混在一起,两边都做不好。
先看实时库存表的核心结构:
CREATE TABLE `inventory_real_time` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`exchange` varchar(16) NOT NULL COMMENT '交易所代码',
`symbol` varchar(32) NOT NULL COMMENT '交易对',
`account_id` varchar(64) NOT NULL COMMENT '账户ID',
`asset_type` tinyint(4) NOT NULL COMMENT '资产类型:1-币,2-合约,3-期权',
`available_qty` decimal(32,8) NOT NULL DEFAULT '0.00000000' COMMENT '可用数量',
`frozen_qty` decimal(32,8) NOT NULL DEFAULT '0.00000000' COMMENT '冻结数量',
`total_qty` decimal(32,8) NOT NULL COMMENT '总数量 = 可用 + 冻结',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`update_time` datetime(3) NOT NULL COMMENT '更新时间,精确到毫秒',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_exchange_symbol_account` (`exchange`,`symbol`,`account_id`),
KEY `idx_update_time` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实时库存表';
这里有个细节,total_qty 我用了 decimal(32,8)。为什么是32位整数加8位小数?我在项目中遇到过,有些交易所的精度要求极高,比如XRP这种,数量大、精度高。32位整数够用,8位小数也覆盖了绝大多数场景。
核心原则:库存表不要存「逻辑库存」,只存「物理库存」。逻辑库存由上层业务计算,底层只保证加减正确。
字段规范与约束
字段命名这件事,看似简单,其实坑很多。我见过有人用 qty、quantity、amount 混着用,最后查问题查得想哭。
我的规范是这样的:
- 数量字段:统一用
qty后缀,比如available_qty、frozen_qty - 金额字段:统一用
amt后缀,比如cost_amt、profit_amt - 价格字段:统一用
price后缀,比如avg_price、last_price - 时间字段:统一用
_time后缀,并且精确到毫秒
约束方面,我建议强制使用 NOT NULL。为什么?因为 NULL 值在库存计算中就是个定时炸弹。你做个加法,一个字段是 NULL,结果直接变成 NULL,整个库存就崩了。
注意:decimal 类型不要用 decimal(65,30) 这种夸张的精度。精度越高,性能越差。32位整数加8位小数,足够用了。我曾经接手过一个项目,所有金额字段都用 decimal(65,30),查询慢得像蜗牛,改完之后性能提升了3倍。
主键与唯一索引策略
主键选什么?自增ID还是业务主键?
我的答案是:自增ID做主键,业务字段做唯一索引。
为什么?自增ID在InnoDB中写入性能最好,因为它是顺序的,不会频繁触发页分裂。而业务主键(比如交易所+交易对+账户)往往很长,作为主键会导致二级索引变得臃肿。
来看唯一索引的设计:
-- 实时库存唯一索引
UNIQUE KEY `uk_exchange_symbol_account` (`exchange`,`symbol`,`account_id`)
-- 快照库存唯一索引
UNIQUE KEY `uk_exchange_symbol_account_snapshot` (`exchange`,`symbol`,`account_id`,`snapshot_time`)
这里有个小技巧:唯一索引的字段顺序,要把区分度高的放前面。比如 exchange 只有几个值,symbol 可能有几百个,account_id 可能有几千个。所以顺序应该是 exchange、symbol、account_id,这样索引的过滤效果最好。
避坑指南:我曾经在唯一索引中包含了 status 字段,结果同一个账户同一个交易对,因为状态不同,插入了两条记录。库存对账的时候,怎么都对不上。后来我把 status 从唯一索引中去掉,问题才解决。
快照库存表设计
快照库存表,说白了就是给库存数据拍「照片」。每5分钟拍一张,出了问题可以回滚到任意时间点。
CREATE TABLE `inventory_snapshot` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`exchange` varchar(16) NOT NULL COMMENT '交易所代码',
`symbol` varchar(32) NOT NULL COMMENT '交易对',
`account_id` varchar(64) NOT NULL COMMENT '账户ID',
`snapshot_time` datetime(3) NOT NULL COMMENT '快照时间',
`available_qty` decimal(32,8) NOT NULL COMMENT '快照可用数量',
`frozen_qty` decimal(32,8) NOT NULL COMMENT '快照冻结数量',
`total_qty` decimal(32,8) NOT NULL COMMENT '快照总数量',
`checksum` varchar(64) NOT NULL COMMENT '数据校验和',
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_snapshot` (`exchange`,`symbol`,`account_id`,`snapshot_time`),
KEY `idx_snapshot_time` (`snapshot_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存快照表';
注意这个 checksum 字段。我习惯把整条记录的所有数字字段拼接起来,算一个MD5。这样在做数据一致性校验时,直接比对 checksum 就行,不用逐字段对比,效率高很多。
知识体系结构图
下面这张图,是我对库存数据模型设计的整体理解:
实战中的几个坑
最后,分享几个我在实战中踩过的坑:
- 不要用浮点数存库存。float/double 会有精度丢失,0.1 + 0.2 不等于 0.3。必须用 decimal。
- 唯一索引不要包含状态字段。状态变了,唯一索引就失效了,会导致重复数据。
- 快照表一定要有 checksum。没有 checksum,对账的时候要逐字段对比,性能差得离谱。
- 主键不要用 UUID。UUID 是随机字符串,写入时会导致频繁的页分裂,性能下降 50% 以上。
我的习惯:每次上线前,我都会在测试环境跑一遍库存对账脚本。如果对账结果不一致,坚决不上线。这个习惯救了我好几次,有一次就是因为唯一索引设计不合理,导致库存数据重复,还好在测试环境发现了。
库存数据模型设计,说难不难,说简单也不简单。关键是要想清楚:你的数据是给谁用的?实时交易用,还是事后对账用?不同的场景,设计思路完全不同。嗯,今天就聊到这里,希望对你有帮助。