目录
一、答案不是“用户字段”或“时间字段”二选一
(一)推荐结论及适用前提
1、常规电商订单的推荐组合
2、为什么不能只回答“按订单号分表”
3、为什么也不能只回答“按时间分表”
(二)一句话区分四个容易混淆的概念
1、分库分片键
2、分表键或分区键
3、主键
4、二级索引与查询副本
二、分片键选择的本质:让最贵的业务动作保持局部性
(一)先列查询,再谈字段
1、把真实负载分成三类
2、建立查询清单与权重
(二)一个可落地的评分模型
1、候选键的六项指标
2、容量决定分片数,而不是“行业惯例”
三、常见分片字段逐一评估
(一)按 buyer_id 或 user_id 哈希
1、优势
2、风险与修正
(二)按 order_id 哈希
1、适合的场景
2、隐含代价
(三)按 created_at 时间范围
1、它真正擅长什么
2、为什么交易主表通常不只按时间
(四)按 seller_id、租户或区域
1、业务归属优先于“用户”字面
2、避免大租户独占热点
(五)复合键与二维拆分
1、复合索引不等于复合分片
2、推荐的二维结构
四、推荐的数据模型与路由实现
(一)逻辑表必须显式携带路由信息
1、订单主表字段建议
2、时间字段要按语义分开
(二)订单号、逻辑桶与扩容
1、订单号的三种路由方式
2、不要依赖各分片自增主键
(三)关联表共置与事务边界
1、需要共置的数据
2、不必强行共置的数据
五、按时间查询所有用户订单:第一条路径是受控的 Scatter-Gather
(一)查询网关如何执行
1、路由与下推
1.1 查询计划
1.2 分片 SQL
2、为什么每个分片取 Top-K 足够
(二)排序、分页与计数
1、K 路归并
1.1 排序契约
1.2 内存边界
2、不要做深 OFFSET
3、精确总数是昂贵功能
(三)一致性与故障语义
1、先定义“所有”是什么意思
2、可接受的三种模式
3、部分失败不能悄悄当成功
六、当全局时间查询成为常态:建立面向读取的第二条数据路径
(一)全局时间索引与查询副本不是一回事
1、轻量路由索引
2、完整查询投影
3、搜索引擎与分析数据库的选择
(二)用 CDC 或事务外盒构建派生数据
1、避免脆弱的业务双写
1.1 日志型 CDC
1.2 事务外盒
2、下游必须幂等
3、对账与修复是正式能力
(三)不同查询的推荐落点
1、实时小窗口列表
2、多条件运营搜索
3、聚合看板
4、全量导出与审计
七、时间分区、冷热分层与历史数据治理
(一)时间分区的正确角色
1、分区裁剪降低单分片扫描量
2、分区不是索引替代品
3、控制分区数量
(二)热、温、冷三层
1、热数据
2、温数据
3、冷数据
八、把方案放到同一张决策表中比较
(一)四种主方案
1、用户哈希分片
2、订单号哈希分片
3、时间范围分片
4、用户分片加时间查询副本
(二)场景化建议
1、中小规模业务
2、成长型电商
3、平台型市场
4、审计与流水系统
九、实施路线:从观测、迁移到可逆切换
(一)上线前验证
1、采集而不是猜测
2、压测必须包含坏路径
(二)迁移步骤
1、建立稳定路由层
2、全量搬迁加增量追平
3、灰度切读再切写
4、退出旧路径
十、最容易踩的坑与设计审查清单
(一)十二个高频误区
1、把“分表字段”当成单一答案
2、把物理分片数写进取模公式
3、订单号与路由强耦合到物理库号
4、全局时间查询串行扫库
5、没有 (time, unique_tiebreaker)
6、用深 OFFSET 做后台导出
7、把 updated_at 当创建口径
8、查询副本没有版本字段
9、认为“最终一致”无需对账
10、把所有关联表都跨分片 Join
11、过度细分时间分区
12、在规模尚小时先承担分布式复杂度
(二)上线评审清单
1、数据与路由
2、查询与索引
3、一致性与运维
十一、最终建议:把冲突的访问模式分层解决
(一)可直接采用的默认蓝图
1、交易层
2、分片内组织
3、全局查询层
(二)真正需要团队拍板的不是字段名
1、三项业务决策
2、三项工程底线
可参考的文章与官方资料
1. 分片、路由与结果归并
2. 数据库分区与索引
3. 查询副本、增量同步与分页
干货分享,感谢您的阅读!
多数以“买家查看自己的订单、按订单号处理单笔订单”为主的交易系统,应优先把稳定的buyer_id(或包含租户维度的业务归属键)作为一级分片依据,通过哈希或虚拟桶把同一买家的订单放到同一数据分片;order_id负责全局唯一与辅助路由,created_at负责分片内索引、时间分区和生命周期管理。若要按时间段查询所有用户的订单,小结果集可以由查询网关并行访问相关分片、在各分片下推过滤和 Top-K,再做流式归并;高频、长时间跨度、聚合或导出类查询,应通过 CDC/事件流构建按时间组织的查询副本,而不是反过来牺牲交易主链路。
分片键不是由表中哪个字段“看起来最均匀”决定,而是由最重要、最频繁且最需要事务一致性的访问路径决定。
一、答案不是“用户字段”或“时间字段”二选一
(一)推荐结论及适用前提
1、常规电商订单的推荐组合
对于典型的 B2C 或平台买家侧订单,推荐把逻辑分片设计为:
bucket_id = hash(tenant_id, buyer_id) mod V physical_shard = bucket_map[bucket_id]其中,V是数量固定且远大于物理分片数的虚拟桶数量,例如 1024、4096 或 16384;bucket_map把虚拟桶映射到当前物理库。数据库内再为时间访问建立(created_at, order_id)、为用户订单列表建立(buyer_id, created_at, order_id)等索引。订单主表、订单明细、支付意图、履约主记录等强关联数据携带同一个bucket_id,尽可能共置于同一物理分片。
这套选择成立的前提是:绝大多数在线请求天然携带buyer_id或能从可信上下文取得它;单个买家的订单规模没有大到独占一个分片仍无法承载;交易更新主要围绕一张订单及其共置数据完成;跨全部用户的时间查询不是每个前台请求都要同步完成的大扫描。
2、为什么不能只回答“按订单号分表”
order_id很适合做全局唯一主键,也适合单笔订单定位,但它未必适合作为唯一分片键。若每个订单号近似随机地分散,某个用户的订单历史会散落在所有分片,用户订单列表、售后中心以及按用户汇总的查询都会变成广播。反过来,如果order_id中包含稳定的逻辑桶位,或者系统维护order_locator(order_id, bucket_id),订单号就能作为第二条精确路由路径,而不破坏按用户聚合的数据局部性。
3、为什么也不能只回答“按时间分表”
按月或按日拆表能很好地裁剪时间范围,但最新时间段承受几乎全部写入,容易产生热点;一个用户的订单历史会跨越多个时间分区;跨月退款、状态更新、履约回写会持续修改旧分区;月初切表、迟到事件和时区边界还会增加运维复杂度。因此,时间通常更适合作为分片内分区键、索引键、归档键或查询副本的组织键,而不是交易主库唯一的一级分片键。
(二)一句话区分四个容易混淆的概念
1、分库分片键
它决定一行数据落在哪个独立数据库或物理分片,直接影响单分片事务、故障域、扩容搬迁和跨分片查询数量。分片键最好稳定、可从请求中获得、分布相对均衡,并与高频访问路径一致。
2、分表键或分区键
它决定数据在同一数据库实例内落入哪张物理表或哪个原生分区。它主要影响单表规模、分区裁剪、DDL、归档与索引维护。分库和分表可以使用不同层次的键,例如库按用户哈希、库内按创建月份做范围分区。
3、主键
主键保证一行的身份与唯一性,但不必天然承担物理路由。分布式系统中可以生成全局唯一order_id,同时仍按buyer_id或bucket_id落库。Apache ShardingSphere 的主键生成文档也强调,分片后各节点本地自增值会相互不可见,因此需要分布式唯一键方案或专门的键生成服务。
4、二级索引与查询副本
二级索引解决“已经按 A 分片,却要按 B 查”的问题。它可以是库内普通索引、跨分片定位表、搜索索引,也可以是面向运营查询的明细宽表。Vitess 把此类跨分片路由能力抽象为 Secondary Vindex:当条件不含主分片键时,二级映射可缩小目标分片集合;没有二级映射时,查询只能散发到全部分片。二级路由并不取代底层普通索引,两者职责不同。
二、分片键选择的本质:让最贵的业务动作保持局部性
(一)先列查询,再谈字段
1、把真实负载分成三类
订单系统的访问通常同时包含三种负载:
在线交易负载:创建订单、支付回调、取消、退款、发货、签收和状态机推进,要求低延迟、幂等与较强一致性。
在线查询负载:用户订单列表、订单详情、客服按订单号查单、商家待发货列表,要求稳定分页与秒级响应。
分析与批处理负载:某时间段全部订单、财务对账、经营报表、风险扫描、监管导出,数据量大、条件多,往往允许秒级到分钟级延迟。
最常见的设计错误,是拿分析负载的“按时间扫全量”要求去决定交易主库的分片方式,结果让每一次下单和每一次用户查单都为低频后台任务付费。正确做法是先保护主交易链路,再为跨分片读建立专门的访问路径。
2、建立查询清单与权重
可以把每类请求记录为一个六元组:〈频率,峰值并发,返回行数,一致性等级,延迟目标,是否携带候选键〉。例如:
| 查询/操作 | 典型条件 | 一致性 | 结果规模 | 建议路由 |
|---|---|---|---|---|
| 创建订单 | buyer_id | 强 | 1 单 | 单分片事务 |
| 用户订单列表 | buyer_id + time/status | 读己之写或最终 | 数十行 | 单分片索引查询 |
| 订单详情 | order_id,常伴随用户上下文 | 强/较强 | 1 单 | ID 解码或定位表 |
| 商家待发货 | seller_id + status | 秒级可见 | 数十至数百行 | 商家侧投影 |
| 全站一小时订单 | created_at | 依用途确定 | 数万至数百万行 | 并行聚合或查询副本 |
| 月度财务对账 | paid_at/settled_at | 可审计快照 | 大批量 | 离线/分析存储 |
不要用“SQL 条数”做权重。一天执行一次、却会读取数十亿行并拖垮主库的任务,重要性可能高,但不应与每秒数万次的用户查单共用相同执行路径。
(二)一个可落地的评分模型
1、候选键的六项指标
对buyer_id、order_id、created_at、seller_id、region_id等候选字段,可以按以下指标打分:
路由覆盖率:高频 SQL 中有多少比例天然携带该字段。
数据均匀性:不同键值对应的数据量和写入量是否均衡。
事务局部性:一次业务事务涉及的数据能否落在同一分片。
稳定性:字段是否可能变更;变更是否意味着跨分片搬行。
扩容成本:增加物理分片时需要搬迁多少数据、改多少路由规则。
次要访问代价:不含该键的查询需要广播、索引映射还是另建读模型。
可以给交易延迟、可用性和事务局部性更高权重,给后台扫描较低权重。评分并不会自动替代架构判断,但能暴露争论双方隐含的业务假设。
2、容量决定分片数,而不是“行业惯例”
假设总数据量为D,单分片可安全承载的数据量为C_d,峰值查询和写入分别为Q、W,单分片安全吞吐为C_q、C_w,目标利用率为U_d、U_q、U_w,初始物理分片数至少应满足:
S >= max( ceil(D / (C_d × U_d)), ceil(Q / (C_q × U_q)), ceil(W / (C_w × U_w)) )还要为副本故障、促销峰值、索引重建和在线迁移保留余量。先根据压测和容量模型得到S,再选择虚拟桶数V。直接把hash(user_id) mod S写死在业务代码里,扩容时S一变就会让大量键重新映射;使用稳定虚拟桶和独立映射表,可以把扩容变成搬迁部分桶,而非全量洗牌。
交易平面按用户归属保持单分片局部性;查询平面按时间和检索字段组织,二者由可重放的数据变更链路连接。
三、常见分片字段逐一评估
(一)按buyer_id或user_id哈希
1、优势
同一用户的订单天然共置,订单列表、售后记录和用户级限额容易在单分片完成;哈希可以打散连续用户号;用户 ID 通常创建后不再变化;订单及明细能够共享同一个路由键。对于“绝大多数流量来自买家端”的系统,这是最稳妥的默认值。
2、风险与修正
风险一是超级用户、企业采购账号或测试账号产生热键。可在业务上拆分组织账号与操作者账号,或对确认存在的超大主体使用受控盐值和独立路由,而不应一开始就给所有用户随机加盐,因为随机盐会破坏用户查询的单分片性质。
风险二是客服只拿到order_id。解决方案是让订单号携带逻辑桶位,或维护小而精确的订单定位表。风险三是商家侧查询天然按seller_id,这说明订单事实表的买家路由不能同时优化商家视图,应构建seller_order_projection,而不是在一张表上同时声称拥有两个主分片键。
(二)按order_id哈希
1、适合的场景
如果系统绝大多数请求都是单订单点查与状态更新,且用户订单列表由另一个索引服务承接,按order_id哈希可以得到非常均匀的写入分布。某些订单中台或履约处理平台只处理订单消息,不直接服务用户历史列表,这时它可能是合理选择。
2、隐含代价
用户、商家、门店或渠道维度的订单会散布到所有分片;批量读取一个用户的十个订单可能触达十个分片;订单明细必须通过同一个order_id共置,否则一次详情查询会出现跨分片关联;当外部系统只提供业务订单号而非内部 ID 时,还需要唯一性和映射策略。
(三)按created_at时间范围
1、它真正擅长什么
时间范围分片让时间查询可以直接裁剪历史分片,旧分片也便于冻结、归档和迁移到低成本介质。MongoDB 的官方文档明确区分范围与哈希分片:范围分片把相邻键放在一起,有利于范围访问;哈希分片更利于写入分散,却会让逻辑相邻的数据分布到多个分片。这个取舍同样适用于关系型订单系统。
2、为什么交易主表通常不只按时间
最新时间窗是持续写热点;用户历史跨时间分片;订单状态在创建后数天甚至数月仍会变化;退货和追溯可能访问很旧的数据;按月切分还要处理月底边界、未来分区预建和补录订单。若全站时间查询才是产品的核心,而单用户事务非常少,例如只追加不可变事件的审计平台,时间可以上升为一级分片键;但那已经不是典型订单交易主库。
(四)按seller_id、租户或区域
1、业务归属优先于“用户”字面
B2B SaaS 的最强隔离边界可能是tenant_id;商家 ERP 的主入口可能是seller_id;数据驻留要求可能强制以region_id先分区。这时分片键应写成业务归属模型,而不是机械套用user_id。常见组合是先按区域/租户分片组,再在组内对买家或商家哈希。
2、避免大租户独占热点
纯tenant_id分片会让头部租户成为单点热点。可采用hash(tenant_id, entity_id),同时通过租户到分片组的映射实现隔离;对必须单租户导出的任务,查询服务知道该租户可能覆盖哪些桶。关键是把“合规隔离边界”和“负载均衡单位”分开建模。
(五)复合键与二维拆分
1、复合索引不等于复合分片
(buyer_id, created_at)作为数据库复合索引非常自然,因为等值用户条件在前、时间范围在后。MySQL 的多列索引遵循最左前缀规律:索引(buyer_id, created_at, order_id)可服务仅含buyer_id或包含前两列的条件,却不能高效支持只按created_at的全站查询。这正是为什么单库索引无法消除跨分片路由问题。
2、推荐的二维结构
一级按用户虚拟桶决定数据库,二级在分片内按月范围分区或按时间索引。它同时做到:用户请求只进一个库;时间条件能在每个库内裁剪不相关月份;旧数据可按月归档。但它不会把“全体用户时间查询”变成单分片查询,查询仍需访问所有用户分片,只是每个分片读取更少的数据。
四、推荐的数据模型与路由实现
(一)逻辑表必须显式携带路由信息
1、订单主表字段建议
CREATE TABLE orders ( bucket_id SMALLINT NOT NULL, order_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, status SMALLINT NOT NULL, amount_cent BIGINT NOT NULL, created_at DATETIME(6) NOT NULL, paid_at DATETIME(6) NULL, updated_at DATETIME(6) NOT NULL, version INT NOT NULL, PRIMARY KEY (bucket_id, order_id), KEY idx_buyer_time (buyer_id, created_at DESC, order_id DESC), KEY idx_created_order (created_at DESC, order_id DESC), KEY idx_status_time (status, created_at, order_id) );这是一份逻辑示例,不应不经压测直接复制。bucket_id是否进入主键、是否采用数据库原生分区、二级索引数量及字段顺序,都要结合所用数据库版本、行宽、写放大和真实 SQL 验证。如果在 MySQL InnoDB 上使用原生分区,还要特别检查两个限制:分区表达式涉及的列必须包含在每一个唯一键中;分区表与外键不兼容。很多订单系统因此选择应用层保证关联完整性,或者采用物理子表而非原生分区。
2、时间字段要按语义分开
“查询某段时间的订单”必须说明是哪一种时间:
created_at:订单事实创建时间,适合下单量和新订单流。paid_at:支付成功时间,适合收款口径,但会为空且可能晚于创建。updated_at:最后修改时间,适合增量同步水位,不适合稳定业务统计。settled_at:结算入账时间,适合财务口径。event_time:某个订单事件真正发生的时间,可能晚到或乱序。
把所有语义塞进一个“订单时间”会导致报表无法对账。时间统一以 UTC 存储,边界使用半开区间[start, end);例如查询 9 月 1 日应写成>= 2026-09-01T00:00:00Z AND < 2026-09-02T00:00:00Z,展示时再转换到业务时区。半开区间能避免毫秒、微秒精度差异和相邻窗口重复。
(二)订单号、逻辑桶与扩容
1、订单号的三种路由方式
第一种是 API 总能携带buyer_id,订单号只承担唯一标识;这是最简单、最安全的方式。第二种是在订单号中编码版本、时间和逻辑桶位,网关可从 ID 解出bucket_id;逻辑桶到物理分片的映射可以变化,因此不要把易变的物理库号永久写进 ID。第三种是维护order_locator:order_id -> bucket_id,它应高可用、可缓存并可从订单变更日志重建。
2、不要依赖各分片自增主键
不同分片的本地自增序列会碰撞。可选择时间有序的 64 位 ID、UUIDv7 类时间有序标识、集中号段服务或数据库提供的全局序列。若使用含机器位与时间位的算法,要处理时钟回拨、机器号冲突、突发序列耗尽和安全暴露问题;ID 是否大致有序与分片键选择是两项独立决策。
外部订单号保持稳定,内部逻辑桶提供可迁移的路由层;扩容只调整桶映射,不改订单身份。
(三)关联表共置与事务边界
1、需要共置的数据
订单主表、订单行项目、订单地址快照、金额明细、订单状态版本等,在创建和核心状态机中经常一起读写,应携带相同的bucket_id。ShardingSphere 的路由文档把具有绑定关系的表视为可按相同节点组合执行;未建立绑定关系的跨分片关联可能膨胀为笛卡尔路由,性能风险显著。
2、不必强行共置的数据
商品主数据、用户档案、优惠券定义和商家资料有自己的生命周期与所有权。订单应保存成交时必须冻结的快照字段,避免每次查历史订单都跨服务关联。优惠券核销、库存扣减和支付处理常属于不同服务,应该通过明确的本地事务、幂等消息、补偿与状态机协调,而不是假设分表以后还能依赖一个巨大的数据库事务。
五、按时间查询所有用户订单:第一条路径是受控的 Scatter-Gather
(一)查询网关如何执行
1、路由与下推
当查询只有created_at而没有主分片键时,按用户分片的系统无法凭空定位一个库。Apache ShardingSphere 的路由说明也指出,不携带分片键的 SQL 会采用全路由。正确的在线实现不是串行循环所有库,而是由查询协调器完成以下步骤:
1.1 查询计划
校验时间范围、权限、允许的状态和最大返回量。
根据时间范围确定每个物理分片内需要访问的时间分区。
使用有上限的并发扇出,而不是无限并发。
把过滤、投影、局部聚合、排序和
LIMIT下推到每个分片。对局部有序结果执行 K 路归并,产生全局顺序。
返回结果、游标、完整性状态和查询快照标识。
1.2 分片 SQL
每个分片上的明细查询可采用:
SELECT order_id, buyer_id, status, amount_cent, created_at FROM orders WHERE created_at >= :start_time AND created_at < :end_time AND (created_at, order_id) < (:cursor_time, :cursor_order_id) ORDER BY created_at DESC, order_id DESC LIMIT :local_limit;首屏没有游标时省略游标条件。order_id是相同时间戳下的稳定决胜字段;若仍可能重复,应再加bucket_id。查询字段必须有覆盖或高选择性索引,且只选择页面需要的列,避免把大 JSON、地址详情等宽字段带入归并层。
2、为什么每个分片取 Top-K 足够
若目标是全局最新 K 条,而且所有分片都按同一键降序,则任何分片中排在本分片第 K 条之后的记录,都不可能进入全局前 K。因此各分片取最多 K 条、再从至多K × S条候选中归并即可。若还包含额外过滤,则过滤必须在分片内先执行。对于深分页,不能简单地在每个分片取当前页大小,而应使用上一页末尾的全局排序键作为游标继续推进。
查询网关只访问时间范围相关的分区,在各分片完成局部排序与限流,再做流式归并。
(二)排序、分页与计数
1、K 路归并
每个分片返回一个已经按(created_at DESC, order_id DESC, bucket_id DESC)排序的流。协调器把每个流的当前头元素放入优先队列,每次弹出全局最大项,再从对应流读取下一项并放回队列。其内存可以与分片数和页面大小相关,而不必把全量结果全部装入内存。ShardingSphere 的归并引擎也采用优先队列合并多个有序结果集,这为该实现提供了直接参照。
1.1 排序契约
所有分片必须使用相同的字段、方向、空值规则、字符集排序规则和时间精度;否则“每个分片内部有序”并不能推出归并结果正确。游标还应包含路由版本或快照标识,避免扩容迁桶过程中把同一逻辑桶读两次。
1.2 内存边界
协调器只缓存每条分片流的头部和必要的预取批次,优先队列规模约为活跃分片数。对慢分片设置独立超时与小批量预取,既避免一个慢节点无限阻塞,也避免过度预取挤占堆内存。
2、不要做深OFFSET
LIMIT 1000000, 20在分布式系统中通常意味着每个分片都可能读取并传输大量候选,协调器再丢弃绝大部分数据。应使用键集分页:下一页条件基于上一页最后一条的复合排序键。Elasticsearch 的官方分页指南同样建议深分页使用search_after,并配合时间点快照和唯一决胜字段,以避免刷新期间出现漏项或重复项。
3、精确总数是昂贵功能
页面顶部的“共 12,345,678 条”需要所有分片完成精确COUNT,其成本可能远高于取 20 条明细。产品可以区分:精确总数、超过阈值后的下界(如“10000+”)、估算值、或完全不展示总数。财务口径的精确总数应走批处理快照;交互检索通常无需每次精确计数。
稳定游标由时间、订单号和逻辑桶共同组成;优先队列一次只推进产生当前结果的那个分片流。
(三)一致性与故障语义
1、先定义“所有”是什么意思
“所有用户的订单”可能有四种含义:查询发起时已经提交的全部订单;各副本在各自可见水位上的订单;某个 CDC 位点之前的订单;财务结账快照中的订单。跨多个独立数据库,很难用一次普通查询获得免费的全局强一致快照。必须让接口声明一致性等级,而不是只给一个模糊的“实时”标签。
2、可接受的三种模式
运营检索:允许秒级延迟,优先读查询副本,返回数据水位。
客服查单:先查索引;若刚创建订单未同步,可回源交易分片,提供读己之写兜底。
财务与审计:固定截止位点,等待各分片达到水位后生成不可变快照,保留校验和与任务版本。
3、部分失败不能悄悄当成功
若 32 个分片中有 1 个超时,接口不能把 31 个分片的结果冒充完整答案。可以按用途选择全部失败、返回partial=true并列出缺失分片、或转异步重试。无论选择哪种,都要记录查询 ID、分片覆盖率、时间水位、重试次数和截断信息。
六、当全局时间查询成为常态:建立面向读取的第二条数据路径
(一)全局时间索引与查询副本不是一回事
1、轻量路由索引
轻量全局索引只保存created_at、order_id、bucket_id和少量过滤字段。查询先从索引得到候选订单与分片位置,再批量回源。这适合结果很少、详情要求读最新状态的场景。代价是两段查询和索引一致性维护;如果时间范围命中百万订单,先取百万定位记录再回源依然昂贵。
2、完整查询投影
查询投影保存页面与报表所需的大部分字段,按时间分区并按常用过滤维度排序,查询无需逐单回源。它适合运营后台、搜索、看板和导出。投影不是交易事实的第二个写主库,而是可从变更日志重建的派生数据;字段定义、延迟目标、纠错和重放机制必须明确。
3、搜索引擎与分析数据库的选择
多条件检索、模糊搜索和交互筛选更适合搜索引擎;时间范围扫描、列式聚合和大规模导出更适合分析数据库;严格按订单号定位则只需要小型全局路由表。ClickHouse 文档把分区首先视为数据管理手段,并说明只有查询能裁剪到少量分区时才可能获益;分区基数过高会产生过多数据部件。因此按月/按日分区、再用排序键组织常用过滤字段,通常比“每个用户一个分区”合理。
数据规模、查询频率、延迟与一致性共同决定路径;没有一种存储同时在四个维度上免费最优。
(二)用 CDC 或事务外盒构建派生数据
1、避免脆弱的业务双写
在同一个请求中先写订单库、再直接写搜索或分析库,任何一次超时都可能造成一边成功一边失败。更可靠的做法是捕获数据库提交后的变更日志,或在本地事务中同时写订单与 outbox 事件,再由独立管道投递。Debezium 的架构文档展示了通过 Kafka Connect 把数据库变更传入消息系统并由下游接收的典型链路,其 Outbox Event Router 则提供了事件外盒转换方式。
1.1 日志型 CDC
日志型 CDC 直接读取数据库已提交的变更,业务侵入较小,适合构建订单明细镜像;但下游看到的是行级变化,需要正确解释事务边界、表结构变更和删除事件。
1.2 事务外盒
事务外盒把业务事件与订单更新放进同一本地事务,事件语义更清晰;代价是需要设计事件模式、清理外盒表,并保证发布器能够重试和重放。二者可以组合:业务写 outbox,CDC 负责可靠抽取。
2、下游必须幂等
即使消息系统提供较强处理保证,跨数据库落地仍要按至少一次投递来设计。查询投影以order_id为幂等键,以version或业务事件序号拒绝旧更新;删除使用墓碑或显式状态;失败进入可重放队列。对乱序事件,不能只比较消费时间,要比较订单版本或事件序号。
3、对账与修复是正式能力
生产系统要持续监控:源库提交位点、各下游消费位点、端到端延迟、重复率、失败率和字段映射版本。定期按时间窗比较源分片与查询副本的count、sum(amount)、min/max(order_id)等摘要,对不一致的桶或时间窗执行精确回查和重放。没有对账闭环的“最终一致”只是无法证明的一致。
变更捕获、幂等投影、水位监控、摘要对账与定向重放组成完整闭环。
(三)不同查询的推荐落点
1、实时小窗口列表
例如查询最近 5 分钟、最多 100 条异常订单,可走受控 Scatter-Gather,前提是分片数有限、索引完备、有严格超时和并发隔离。若该查询每秒大量执行,应迁移到查询副本。
2、多条件运营搜索
按时间、手机号后四位、商家、状态、渠道、风险标记组合筛选,适合搜索索引或专用宽表。敏感字段需要脱敏、权限过滤和审计;搜索结果跳转详情时可按订单号回源验证最新状态。
3、聚合看板
对COUNT/SUM/MIN/MAX等可分解聚合,可在各分片或流处理中先做局部聚合,再汇总;去重人数、分位数等不可简单相加的指标需要明确算法和误差。高频看板应按分钟或小时预聚合,避免每次扫描明细。
4、全量导出与审计
导出不应由 HTTP 请求长时间占用连接。提交异步任务,记录查询条件与一致性水位,分段读取、生成文件、校验条数与摘要,再通知下载。对审计任务保存不可变清单,能够说明数据来自哪些分片、截至哪个位点、是否发生补跑。
七、时间分区、冷热分层与历史数据治理
(一)时间分区的正确角色
1、分区裁剪降低单分片扫描量
MySQL 将分区裁剪概括为不扫描不可能包含匹配值的分区;PostgreSQL 同样根据分区边界排除不相关分区,并提醒分区键应经常出现在查询条件中。把订单分片内按月分区,可以让三天范围查询只读相邻月份,而不扫描几年历史。
2、分区不是索引替代品
PostgreSQL 文档明确指出,分区裁剪由分区边界驱动,而不是由索引存在驱动;剩余分区是否还需要索引,取决于查询会读取其中很大比例还是很小比例。换言之,按月分区后仍需为(created_at, order_id)、(status, created_at)等真实过滤和排序建立索引。
3、控制分区数量
过细的日分区甚至小时分区会增加元数据、查询规划、DDL、备份与监控开销。选择粒度时应计算每个分区的行数、大小、生命周期动作频率和典型查询跨度。月订单数极大时可按日;月订单数适中时按月;不要把“越细裁剪越准”当成唯一目标。
(二)热、温、冷三层
1、热数据
最近数月仍频繁更新和查询,保留在交易分片主存储及低延迟副本中。热层索引完整,备份与恢复目标最严格。
2、温数据
状态基本稳定,但客服和用户偶尔查看。可减少非必要索引、迁往成本更低的数据库集群或只读存储,同时通过统一查询网关保持访问接口不变。
3、冷数据
超过在线保留期的历史订单进入对象存储或湖仓格式,用于审计、离线分析和按任务恢复。冷数据仍须遵循保留、删除、加密和访问审计要求。迁移前后要以条数、金额摘要和校验清单验证完整性。
数据位置随更新概率和访问频率变化,统一目录保存时间范围、水位、校验与物理位置。
八、把方案放到同一张决策表中比较
(一)四种主方案
1、用户哈希分片
交易局部性最好,写入较均匀,用户查询简单;全局时间查询需要扇出或派生读取路径。适合绝大多数消费者订单系统。
2、订单号哈希分片
单订单访问与写入均衡优秀,用户/商家聚合较差;适合订单处理中台,或已有成熟多维索引层的系统。
3、时间范围分片
全局时间查询、归档和按期管理优秀,最新分片热点明显,用户历史分散;适合追加型日志、审计流水或时间访问绝对占主导的系统。
4、用户分片加时间查询副本
以额外存储、同步延迟和数据治理换取交易与查询各自优化,是规模化后最常见也最可控的形态。复杂度更高,但复杂度被放在边界清晰、可重建的派生链路中,而不是渗入每个交易请求。
(二)场景化建议
1、中小规模业务
数据仍可由单库或少量分片承担时,不要过早创建数百张表。先使用合理主键、复合索引、只读副本、归档和容量告警;确认单机瓶颈来自写入、存储或维护窗口后再分片。分片会把唯一约束、事务、查询、DDL、备份与排障全部变成分布式问题。
2、成长型电商
采用buyer_id -> virtual bucket -> physical shard,订单号可解逻辑桶或有定位表;分片内为用户列表和时间扫描分别建索引;最近小窗口由查询网关扇出;运营检索通过 CDC 建立查询投影。为超级用户、热点活动和分片迁移预留机制。
3、平台型市场
选择交易事实的主归属方,例如买家;为商家、渠道、客服分别建立派生视图。若商家履约是最核心事务,也可以反向以商家为主,但必须用实际调用量和一致性边界证明。不要在一份订单事实上承诺买家与商家查询都天然单分片。
4、审计与流水系统
如果记录不可变、查询几乎总带时间、写入可按时间桶并行且不需要用户级事务,时间范围或“时间桶 + 哈希槽”可能更合适。即使如此,也要避免单一当前时间桶热点,可以在每个时间桶内再散列多个写分区。
九、实施路线:从观测、迁移到可逆切换
(一)上线前验证
1、采集而不是猜测
至少收集一个完整业务周期的 SQL 模板、条件字段覆盖率、行数、P50/P95/P99 延迟、锁等待、热键、单用户最大订单量、按小时写入曲线和后台任务时间窗。脱敏后对键分布进行回放,计算每个候选分片的存储、QPS 和峰值写入偏差。
2、压测必须包含坏路径
不仅压测buyer_id命中的单分片查询,还要压测缺失分片键、跨分片排序、一个分片慢、一个分片不可用、深分页、热点用户、月末跨分区和 CDC 积压。只有好路径的吞吐数字无法预测生产故障。
(二)迁移步骤
1、建立稳定路由层
把路由规则从业务 SQL 中抽离,所有写入和核心读取经过统一 SDK 或数据库代理;请求日志记录route_version、bucket_id、physical_shard。先在单库上引入逻辑桶字段并验证分布,使应用与未来物理布局解耦。
2、全量搬迁加增量追平
按虚拟桶分批复制历史数据,同时捕获迁移期间的增量变更;对每批数据比较条数、主键集合摘要和业务金额;达到追平水位后进入双读校验。直接暂停全站写入再搬全量通常不可接受。
3、灰度切读再切写
先镜像读取并比较,不影响用户返回;再按租户或桶灰度读取新分片;确认延迟、错误率和一致性后切写。每个阶段都保留明确的回退开关和回退水位。双写阶段若存在,必须短、可观察且有补偿队列。
4、退出旧路径
完成稳定期后冻结旧库,保留审计与回滚所需的只读窗口;确认新链路备份恢复、扩容、DDL、故障演练和对账都通过,才回收旧资源。迁移完成的定义不是“流量切过去”,而是日常运维闭环也能独立运行。
每一步都有校验水位和回退点;路由层、数据复制与业务切换相互解耦。
十、最容易踩的坑与设计审查清单
(一)十二个高频误区
1、把“分表字段”当成单一答案
实际至少要同时设计分库键、库内分区键、主键、二级路由和查询副本。只写一句“按用户 ID 取模”远远不够。
2、把物理分片数写进取模公式
分片数变化引起大面积重映射。应使用虚拟桶或一致性映射层,并对路由规则做版本化。
3、订单号与路由强耦合到物理库号
物理库会扩容、合并或跨地域迁移。订单号最多编码稳定逻辑桶或版本,不应永久暴露易变拓扑。
4、全局时间查询串行扫库
串行延迟接近各分片延迟之和,且没有统一超时和部分失败语义。必须有受控并发、下推和归并层。
5、没有(time, unique_tiebreaker)
仅按时间排序时,同一微秒的多笔订单顺序不稳定,翻页会重复或遗漏。加入订单号和必要的桶位作为稳定决胜字段。
6、用深 OFFSET 做后台导出
越翻越慢,还会受到并发写入影响。采用快照水位、键集分页和异步任务。
7、把updated_at当创建口径
一次退款更新会把老订单推入“今天订单”。统计字段必须与业务事件语义一致。
8、查询副本没有版本字段
乱序消息可能让旧状态覆盖新状态。以业务版本或事件序号进行条件更新,并保留重放能力。
9、认为“最终一致”无需对账
没有水位、摘要校验和修复流程,就无法知道最终何时一致,也无法证明是否丢数。
10、把所有关联表都跨分片 Join
跨分片关联可能放大路由组合和网络流量。强关联订单数据共置,其他域通过快照、服务调用或派生视图连接。
11、过度细分时间分区
分区过多会增加元数据和规划成本。粒度应由分区大小、访问跨度和生命周期动作共同决定。
12、在规模尚小时先承担分布式复杂度
如果单库通过索引、归档、读写分离与垂直拆分即可满足目标,分片不一定是下一步。先证明瓶颈,再选择最小可行的拆分。
(二)上线评审清单
1、数据与路由
主分片键是否在绝大多数交易请求中可得且可信?
分片键是否不可变?若必须变更,搬迁协议是什么?
虚拟桶数量、映射版本和缓存失效机制是否明确?
order_id如何全局唯一,如何从订单号定位分片?超级用户、头部租户和热点活动如何处理?
2、查询与索引
用户订单列表、单订单、全站时间查询分别走哪条路径?
分片内索引是否与等值条件、范围条件和排序顺序匹配?
时间边界是否统一 UTC 与半开区间?
分页是否使用稳定复合游标?
精确计数是否真有产品必要,最大时间跨度和最大导出量是多少?
3、一致性与运维
每个接口的一致性等级、数据水位和部分失败语义是什么?
CDC 是否可重放,下游是否按版本幂等?
是否有按桶、按时间窗的持续对账和定向修复?
单分片慢、分片失联、查询网关过载时如何限流和降级?
扩容、缩容、DDL、备份恢复与冷热迁移是否做过演练?
先判断最强事务归属,再判断全局时间查询的频率和规模,最后选择扇出、索引或分析副本。
十一、最终建议:把冲突的访问模式分层解决
(一)可直接采用的默认蓝图
1、交易层
以稳定业务归属键为主:常规买家订单使用hash(tenant_id, buyer_id)映射到固定虚拟桶,再由虚拟桶映射物理分片。订单与核心明细共置,单笔交易尽量在一个分片完成。订单号全局唯一,但与物理拓扑解耦。
2、分片内组织
为用户列表使用(buyer_id, created_at DESC, order_id DESC);为受控全局扇出使用(created_at DESC, order_id DESC);状态查询根据选择性使用(status, created_at, order_id)。数据量和生命周期需要时,再按月做分区,并核对数据库对唯一键、外键和分区的具体限制。
3、全局查询层
低频、短窗口、小页面:查询网关并行访问所有相关分片,局部 Top-K,优先队列归并,复合游标翻页。高频、多条件、大范围、聚合或导出:CDC/事务外盒构建按时间组织的搜索或分析投影,返回明确数据水位,并以对账和重放保证可证明的一致性。
(二)真正需要团队拍板的不是字段名
1、三项业务决策
第一,谁是订单事实的主归属:买家、商家、租户还是区域;第二,全局时间查询允许多大延迟和多大结果集;第三,查询不完整时是失败、降级还是返回部分结果。只要这三项明确,字段和中间件选择通常会变得清晰。
2、三项工程底线
任何方案都应满足:路由可解释且可演进;分页、时间语义与一致性可验证;派生数据可重建并可对账。分片的目标不是把一张大表切成许多小表,而是把主要事务限制在可控故障域内,同时为不可避免的跨域读取提供专门、可治理的路径。
可参考的文章与官方资料
1. 分片、路由与结果归并
Apache ShardingSphere:Route Engine
Apache ShardingSphere:Merger Engine
Apache ShardingSphere:Key Generate Algorithm
Vitess:Vindexes
Amazon Dynamo:高度可用键值存储论文
2. 数据库分区与索引
MySQL 8.4:Partition Pruning
MySQL 8.4:Multiple-Column Indexes
MySQL 8.4:Partitioning Keys, Primary Keys, and Unique Keys
MySQL 8.4:Partitioning Limitations Relating to Storage Engines
PostgreSQL 18:Table Partitioning
MongoDB:Distribute Collection Data
ClickHouse:Table Partitions
3. 查询副本、增量同步与分页
Debezium:Architecture
Debezium:MySQL Connector
Debezium:Outbox Event Router
Apache Kafka Streams:Core Concepts
Elasticsearch:Paginate Search Results