第三章:数据库表结构设计——订单主表、明细表、库存表

好,咱们进入实战环节了。这一章聊数据库表设计,说白了就是给订单系统搭骨架。骨架搭不好,后面加再多的缓存、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_timeupdate_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_idtotal_stockavailable_stockversion(乐观锁用)。

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 核心逻辑流程图

下面这张图展示了订单创建时,数据在三个表之间的流转关系:

t_order 订单主表 order_id, user_id, status... t_order_item 订单明细表 item_id, order_id, sku_id... t_stock 库存表 sku_id, available_stock... 1:N 扣库存 分库分表规划(ShardingSphere) 分片键:user_id 分库策略:user_id % 16 → ds0 ~ ds15 分表策略:user_id / 16 % 64 → t_order_0 ~ t_order_63 库存表:不分库分表,Redis + 数据库双写

3.7 小结

这一章咱们把订单系统的三张核心表聊透了。订单主表管全局,明细表管商品,库存表管数量。索引策略上,按需添加,别贪多。分库分表用 ShardingSphere,分片键选 user_id,这是经过实战检验的方案。

嗯,表结构设计这块,说白了就是为未来留余地。你想想看,如果一开始就规划好分库分表,后面扩容时只需要改配置,不用改代码,多省心。