一篇读懂分库分表:分片键的选择比一切中间件都重要

分库分表的真正难点

单表数据量过亿、写入持续增长——分库分表是绕不开的选项。但多数分库分表的失败不是败在中间件(ShardingSphere、MyCat 的选型),是败在分片键的选择:分片键选错,之后每一类查询都要广播到所有分片(聚合成本爆炸)、或者出现数据倾斜(热分片)。分片键是「一次决定、终身受用(或受累)」的决策。

这篇拆解:垂直与水平拆分的边界、分片键的设计原则、分片后的限制、以及扩容的代价。

垂直与水平:两种拆分的边界

垂直拆分:按业务维度拆——垂直分库(不同业务的表进不同的库——订单库、用户库、商品库——微服务的数据库拆分——边界是「业务的领域」);垂直分表(一张宽表按字段的热度拆(高频字段一张表、大字段(详情、备注)另一张——「列的冷热分离」——减少热表的单行体积、提升缓存效率。垂直拆分的特征:结构与业务的解耦——不解决单表数据量的问题(订单库垂直拆出来后,订单表还是那一张亿级表——垂直拆解决「业务的隔离」与「列的膨胀」,不解决「行的规模」。

水平拆分:按数据维度拆——同一个表的数据按某个键(分片键)分到多个库表——「行的规模」的解法——每个分片的数据量可控(单表控制在百万到千万级——B+ 树的深度可控、索引效率可控、备份恢复可控)。水平拆分的成本:跨分片查询的复杂性(不按分片键的查询要广播)、分布式事务(跨分片的写一致性)、扩容的数据迁移——水平拆分的每一项收益都伴随一项成本。

决策的顺序:先垂直后水平——先按业务垂直拆(解耦)、再对单表过大的表水平拆(缩量)——跳过垂直直接水平是常见的混乱源(把不相关的业务表水平拆在一起——后续的业务演进处处受限)。

分片键的选择:一切的核心决策

原则一:查询的对齐。分片键要覆盖最高频的查询——「80% 的查询带分片键」是理想状态(订单表按 user_id 分片——「查某用户的订单」单分片命中——先盘点查询(列出系统的核心查询及其过滤条件——再选分片键——「按查询设计分片键」而不是「按直觉」。原则二:分布的均匀。分片键的值要均匀散列(避免热点——「自增 ID 分片」的热点(自增 ID 递增——新数据全进最后一个分片——写热点——「哈希打散」(hash(user_id) % N——分布均匀——但 range 查询(按时间范围查全量)被拆散——「时间分片」的权衡(按时间分片——range 查询高效、时间上均匀——但「最新数据」集中在最新分片——热数据集中的双重性:写入集中(热)但查询也集中(时间敏感查询高效)——业务的时间特征决定是否适合时间分片。原则三:未来的扩容友好。分片键要支持扩容(一致性哈希篇的原理——「取模分片」的扩容灾难(hash % N——N 从 8 变 16 时几乎全部数据要迁移——「一致性哈希」或「基因法」——一致性哈希只迁移 K/N——「基因法」(gene method):把分片基因嵌入 ID(订单 ID 的某些位是用户 ID 的哈希——订单表按订单 ID 分片、但订单 ID 由 user_id 哈希生成——「查用户订单」可以通过 ID 前缀路由——「一个 ID 承载多个分片维度」的设计——避免了「订单按订单 ID 分、查用户订单要广播」的困境。

graph TD
    A[分片键三原则] --> B[查询对齐<br/>80% 查询带分片键 · 先盘点查询]
    A --> C[分布均匀<br/>哈希打散防热点 · 时间分片的双面性]
    A --> D[扩容友好<br/>一致性哈希 · 基因法 ID]
    E[基因法示例] --> F[订单 ID 内嵌 user_id 哈希<br/>订单表按订单 ID 分<br/>用户查询经 ID 前缀路由]

分片之后:那些「再也做不了」的操作

失去一:跨分片的事务。一个事务写多个分片(分布式事务的问题——2PC/TCC/Saga(库内分布式事务篇)——「分片键的设计目标就是避免跨分片事务」——把强相关的数据放同一分片(订单与订单明细按同一键分——「数据同驻(co-location)」的设计原则。失去二:全局的唯一约束。唯一索引只在分片内唯一(user_id 在分片内唯一、跨分片可能重复——「全局唯一」的解法:分布式 ID(库内已有)保证全局唯一、唯一性校验上移到应用层(或用「查一次再插」的软校验。失去三:无分片键的查询。不带分片键的查询要广播到全部分片(聚合成本=N 倍——「广播查询」的治理:禁止性规范(核心查询必须带分片键)、异构索引的补偿(「异构索引」:把数据再按另一个维度存一份(Elasticsearch 按商品维度、按用户维度各存一份——「数据双写+异构索引」的架构——用冗余换查询——冗余的数据一致性(双写的顺序与失败处理——对账系统的兜底(库内对账篇的设计在分片场景的必要)。失去四:跨节点的聚合与分页。跨分片的排序分页(全局 Top-N 要归并所有分片的局部 Top-N——「归并的成本」(深分页的灾难(第 1000 页要每片取前 1000×页——「分页的替代」:游标分页(cursor-based——按 ID 排序的「下一页」——避免深分页——「业务上避免深分页」(导出走离线批处理、页面只提供浅分页。

扩容:分片后的必然考验

扩容的方式:翻倍扩容(N→2N——每个分片的一半数据迁到新分片——「翻倍」的迁移量是 50%——一致性哈希的方式(虚拟节点——迁移量 K/N——「迁移的工程」:双写迁移(新旧分片双写、读旧库、灰度切读——「双写-追平-切读-下线」的四步——迁移工具(ShardingSphere 的数据迁移、自研的对账+迁移脚本——「迁移的对账」(迁移完成度的校验——count 对比、抽样对比——「对账通过才切读」是迁移的安全底线。「一开始就多留余量」:分片数的预留(一开始分 16 片、逻辑分片 1024(「逻辑分片数大于物理实例」——逻辑分片映射到物理实例、扩容时只改映射——「逻辑分片」的扩容友好——不动数据、只改路由——「路由表」的中间层——这是大型系统的常见设计(预留足够的逻辑分片、物理扩容只动映射)。

分库分表的进阶:异构索引与查询路由

分库分表的进阶要解决「查询的多样性」问题。「查询路由」的架构层:「路由中间件」的形态:SDK 模式(ShardingSphere-JDBC——「SDK 的优势与劣势」(无中间件性能损耗、语言绑定(Java——「多语言系统」的困境(Go/Python 服务怎么办——Proxy 模式(ShardingSphere-Proxy——「Proxy 的优势」(语言无关、统一管控——「Proxy 的性能税」(多一跳的转发——「SDK+Proxy」的混合(写入 SDK、复杂查询 Proxy——「混合」的运维成本。「异构索引」的深化:Elasticsearch 的补偿架构——「ES 的索引设计」(按查询维度建索引(用户维度、商家维度——「多维度索引」的存储成本——「双写的一致性」(DB→ES 的同步延迟——「同步的机制」:binlog 订阅(canal/Flink CDC——「CDC 的架构」(DB 的变更→CDC→ES——「CDC 的解耦」(业务代码不感知同步——「CDC 的顺序与丢失」(binlog 的顺序性、断点续传——「ES 的最终一致」(同步延迟的窗口——「延迟窗口」的业务容忍(查 ES 的结果比 DB 旧几秒——「容忍度」的场景判断(列表页可容忍、详情页要准——「详情页走 DB、列表页走 ES」的读写分离。「分库分表的「雪花 ID」协同」:分布式 ID 篇的雪花算法与分片键的关系——「雪花 ID 的机器位」与「分片路由」(雪花 ID 含机器位——「按 ID 路由」的取模——「ID 的基因」设计(基因法篇的延伸——「雪花 ID 与基因法」的结合(机器位即基因——「路由的确定性」(ID 一生成、路由就确定——「路由的零查询」(不用查路由表——「基因嵌入」的优雅。「分库分表的「回迁」与「去分库分表」:新数据库的「分布式单体」反思(「TiDB 类分布式数据库」的新选择(「分布式数据库替代分库分表」的趋势(TiDB/OceanBase——「不用分片的分库分表」——「分布式 DB 的成本」(资源与运维——「趋势的判断」:新系统可以考虑分布式 DB、存量系统的分库分表仍是主流——「两条路线」的并存。分库分表的进阶总结:路由中间件的 SDK/Proxy 选型、异构索引的 CDC 同步、雪花 ID 的基因协同、分布式 DB 的新路线——分库分表不是终点,是「数据架构演进」的一站——「一站式的数据架构」思维:分片、异构、路由、扩容,是一套组合拳,而不是一个单招。

小结

分库分表的决策地图:垂直与水平的边界(垂直解业务隔离与列膨胀、水平解行的规模——先垂直后水平);分片键的三原则(查询对齐——先盘点查询、分布均匀——哈希与时间分片的权衡、扩容友好——一致性哈希与基因法);分片后的四失去(跨分片事务——数据同驻、全局唯一约束——分布式 ID、无分片键查询——异构索引补偿、深分页——游标替代);扩容的工程(双写-追平-切读-下线四步、对账兜底、逻辑分片的预留)。给决策者的最后一句:分库分表是「不可逆」的重构——做完之后,每一类查询的设计都围绕分片键展开——所以这个决策值得花掉一个月做查询盘点与分片键推演——分片键选对的分库分表是资产,选错的是负债——而「负债」的偿还方式,往往只能是再拆一次。

分库分表最难的不是把表拆开,是把「拆开之后系统还能优雅地活着」想清楚——分片键就是那个「想清楚」的结晶。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?