库存数据模型设计:核心库存表结构设计、字段规范与约束、主键与唯一索引策略

做市系统的库存数据,说白了就是你的「家底」。家底都算不清楚,还做什么市?

我见过不少团队,一上来就追求高并发、低延迟,结果库存数据对不上,最后全盘重算。嗯,那场面,真是惨不忍睹。今天咱们就聊聊,怎么把库存表设计得既稳又准。

核心库存表结构设计

我个人习惯,库存表一定要分成两层:实时库存表快照库存表。为什么?你想想看,实时库存要扛住每秒几千次的读写,快照库存要保证历史可追溯。混在一起,两边都做不好。

先看实时库存表的核心结构:

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位小数也覆盖了绝大多数场景。

核心原则:库存表不要存「逻辑库存」,只存「物理库存」。逻辑库存由上层业务计算,底层只保证加减正确。

字段规范与约束

字段命名这件事,看似简单,其实坑很多。我见过有人用 qtyquantityamount 混着用,最后查问题查得想哭。

我的规范是这样的:

  • 数量字段:统一用 qty 后缀,比如 available_qtyfrozen_qty
  • 金额字段:统一用 amt 后缀,比如 cost_amtprofit_amt
  • 价格字段:统一用 price 后缀,比如 avg_pricelast_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 可能有几千个。所以顺序应该是 exchangesymbolaccount_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 就行,不用逐字段对比,效率高很多。

知识体系结构图

下面这张图,是我对库存数据模型设计的整体理解:

库存数据模型设计知识体系 核心库存表 实时库存表 快照库存表 日志流水表 对账差异表 字段规范与约束 命名规范:qty/amt/price NOT NULL 强制约束 decimal(32,8) 精度 时间精确到毫秒 主键与索引策略 自增ID主键 业务字段唯一索引 区分度高的放前面 避免包含状态字段 设计核心原则 物理库存 vs 逻辑库存分离 | 可追溯 | 高性能写入 | 强一致性

实战中的几个坑

最后,分享几个我在实战中踩过的坑:

  1. 不要用浮点数存库存。float/double 会有精度丢失,0.1 + 0.2 不等于 0.3。必须用 decimal。
  2. 唯一索引不要包含状态字段。状态变了,唯一索引就失效了,会导致重复数据。
  3. 快照表一定要有 checksum。没有 checksum,对账的时候要逐字段对比,性能差得离谱。
  4. 主键不要用 UUID。UUID 是随机字符串,写入时会导致频繁的页分裂,性能下降 50% 以上。

我的习惯:每次上线前,我都会在测试环境跑一遍库存对账脚本。如果对账结果不一致,坚决不上线。这个习惯救了我好几次,有一次就是因为唯一索引设计不合理,导致库存数据重复,还好在测试环境发现了。

库存数据模型设计,说难不难,说简单也不简单。关键是要想清楚:你的数据是给谁用的?实时交易用,还是事后对账用?不同的场景,设计思路完全不同。嗯,今天就聊到这里,希望对你有帮助。

专注资料整理