第三章:数据库表结构设计——订单主表、明细表、库存表
好,咱们进入实战环节了。这一章聊数据库表设计,说白了就是给订单系统搭骨架。骨架搭不好,后面加再多的缓存、MQ 都白搭。我在项目中见过太多因为表结构设计不合理,导致后期分库分表痛不欲生的案例。嗯,咱们今天就把这事聊透。
3.1 订单主表:核心中的核心
订单主表,我习惯叫它 t_order。这张表记录的是订单的全局信息,比如谁下的单、什么时候下的、总金额多少、状态如何。
核心字段设计:
order_id:主键,建议用雪花算法生成,别用自增ID。为什么?分库分表时自增ID会冲突,你想想看。user_id:用户ID,分库分表键(sharding key)的首选。order_status:订单状态,用 tinyint,0-待支付、1-已支付、2-已发货……别用字符串。total_amount:总金额,用 decimal(18,2),别用 float,精度问题坑过不少人。create_time、update_time:时间戳,必备字段。
CREATE TABLE `t_order` (
`order_id` bigint(20) NOT NULL COMMENT '订单ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态',
`total_amount` decimal(18,2) NOT NULL COMMENT '订单总金额',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
个人经验:我曾经在项目中把 order_id 设计成自增ID,结果业务量上来后,分库分表时不得不做ID映射,那叫一个痛苦。后来所有新项目我都用雪花算法,省心。
3.2 订单明细表:别小看它
订单明细表 t_order_item,记录每个订单里的商品信息。一张订单可能对应多条明细,比如你买了手机和耳机,那就是两条记录。
这里有个关键点:明细表要不要冗余商品信息?我的建议是——冗余。为什么?因为商品信息可能会变,但订单里的商品名称、价格必须固定。你想想看,用户查看历史订单时,看到的是当时的价格,不是现在的。
CREATE TABLE `t_order_item` (
`item_id` bigint(20) NOT NULL COMMENT '明细ID',
`order_id` bigint(20) NOT NULL COMMENT '订单ID',
`sku_id` bigint(20) NOT NULL COMMENT '商品SKU ID',
`sku_name` varchar(200) NOT NULL COMMENT '商品名称(冗余)',
`price` decimal(18,2) NOT NULL COMMENT '单价(冗余)',
`quantity` int(11) NOT NULL COMMENT '数量',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`item_id`),
KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
避坑指南:我曾经见过有人把明细表设计成无主键表,觉得用 order_id + sku_id 联合唯一就够了。结果后来做数据修复时,没有主键简直寸步难行。记住:每张表都要有独立主键。
3.3 库存表:高并发的命门
库存表 t_stock,这是整个系统里并发最高的表。每次下单都要扣库存,抢购时更是每秒几万次的更新。
核心字段就几个:sku_id、total_stock、available_stock、version(乐观锁用)。
CREATE TABLE `t_stock` (
`sku_id` bigint(20) NOT NULL COMMENT '商品SKU ID',
`total_stock` int(11) NOT NULL COMMENT '总库存',
`available_stock` int(11) NOT NULL COMMENT '可用库存',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
注意:库存表不要用自增ID,直接用 sku_id 做主键。因为库存查询都是基于 sku_id 的,这样能避免一次回表查询。
3.4 索引策略:别乱加索引
索引不是越多越好。我见过有人一张表加了十几个索引,结果写入性能惨不忍睹。咱们来梳理一下核心索引策略:
| 表名 | 索引字段 | 索引类型 | 说明 |
|---|---|---|---|
| t_order | user_id | 普通索引 | 按用户查订单,高频查询 |
| t_order | create_time | 普通索引 | 按时间范围查询,比如查最近30天订单 |
| t_order_item | order_id | 普通索引 | 按订单查明细,必须加 |
| t_stock | sku_id | 主键索引 | 直接做主键,无需额外索引 |
个人习惯:我一般会在建表时就规划好索引,而不是等上线后发现慢查询再加。因为大表加索引会锁表,生产环境上搞一次就够你喝一壶的。
3.5 分库分表方案:ShardingSphere 初步规划
当单表数据量超过千万,或者QPS超过几千,就该考虑分库分表了。我个人比较推荐 ShardingSphere,它支持分库分表、读写分离、分布式事务,而且对业务代码侵入性小。
咱们先做个初步规划:
- 分片键:用 user_id。为什么?因为大部分查询都是按用户来的,按 user_id 分片能保证同一个用户的数据落在一个库一个表里,避免跨库查询。
- 分片策略:用取模。比如分16个库,每个库64张表,那就是 user_id % 16 决定去哪个库,user_id / 16 % 64 决定去哪个表。
- 库存表:库存表不分库分表,因为它是热点数据,分片后反而增加复杂度。用 Redis 做前置缓存,数据库做最终一致性。
# ShardingSphere 配置示例(YAML格式)
sharding:
tables:
t_order:
actualDataNodes: ds$->{0..15}.t_order_$->{0..63}
tableStrategy:
inline:
shardingColumn: user_id
algorithmExpression: t_order_$->{user_id / 16 % 64}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds$->{user_id % 16}
t_order_item:
actualDataNodes: ds$->{0..15}.t_order_item_$->{0..63}
# 与订单表保持一致的分片策略
避坑指南:我曾经在项目中把分片键设计成 order_id,结果按用户查订单时,需要扫描所有分片,性能直接崩了。后来改成 user_id 分片,问题解决。记住:分片键一定要选查询频率最高的字段。
3.6 核心逻辑流程图
下面这张图展示了订单创建时,数据在三个表之间的流转关系:
3.7 小结
这一章咱们把订单系统的三张核心表聊透了。订单主表管全局,明细表管商品,库存表管数量。索引策略上,按需添加,别贪多。分库分表用 ShardingSphere,分片键选 user_id,这是经过实战检验的方案。
嗯,表结构设计这块,说白了就是为未来留余地。你想想看,如果一开始就规划好分库分表,后面扩容时只需要改配置,不用改代码,多省心。