ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

数据库分片实战指南:分表、分库、分区与扩容全解析

2026/9/17 9:44:08 拓冰建站 浏览量
数据库分片实战指南:分表、分库、分区与扩容全解析 先聊点实在的。数据库分片Sharding这词在我见过的项目方案里被用滥了有的系统一天几百QPS就嚷嚷着要分库有的订单表都上亿了还在硬扛。真正动手拆过库、拆过表的人都知道“拆”不是目的“拆完还能正常迭代”才是本事。这篇文章我就把分表、分库、分片、分区这四件事从头到尾捋一遍讲清楚原理、选型、步骤和坑尽量让想上手的人少走点弯路。先说清楚这篇文章适合谁。如果你是后端开发、DBA、架构师或者正在做系统容量规划的技术负责人那这内容基本是为你准备的。就算你只是刚接触数据库的初学者我也尽量把概念讲得接地气一些让你能听懂“为什么要拆”以及“拆了之后怎么收拾残局”。1. 数据库分片到底是什么别急着拆先搞懂一拆问题1.1 为什么好好的数据库要“拆”数据库性能扛不住通常不是“慢”这么简单。你去看那些出问题的系统几乎都逃不过这几类症状单表数据量过大比如千万级、亿级导致索引层级变深B树从三层变成四层每次查询的IO次数明显增加。写入并发太高单库单表的写入锁竞争严重尤其是InnoDB的行锁、间隙锁在热点行上互相等待。历史数据堆积比如订单表存了三年的数据90%是冷数据却和热数据挤在同一张表里。单个实例的CPU、内存、磁盘IO到达瓶颈垂直扩容加配置边际效益越来越低而且钱烧得心疼。这四类问题对应了四种应对手段分区解决“表太大查不动”分表解决“单表写锁竞争激烈”分库解决“单实例容量和连接数不够”分片则是把前面几件事统一设计成一套分布式数据架构。但很多人犯的认知错误是上来就分片把系统搞复杂了回头一看数据量根本没那么大。所以我一直觉得做技术选型之前先给系统做一次“能力体检”而不是无脑引入重方案。1.2 Sharding的四种武器分表、分库、分片、分区一次讲清这四个概念经常被混着说但实际上它们拆的维度完全不同。我画张表帮你理清楚操作拆分维度核心解决典型动作分区Partition在一张表内部按规则把数据分到不同物理文件/分区段单表查询全表扫描慢、数据管理效率低PARTITION BY RANGE (YEAR(create_time))分表Sharding Table把一张表的行或列拆到多张物理表中单表数据量大、写入锁竞争大订单表按user_id拆成order_0、order_1分库Sharding Database把表或数据分到多个数据库实例/库中单实例容量、连接数、IO 压力用户库、订单库、支付库分开或水平拆到多库分片Sharding通过中间件或应用层将数据按规则分布到多个库表突破单机软硬件上限order_db_0.order_0到order_db_9.order_9一句话概括分区是第一阶的“内部分离”分表和分库是第二阶的“物理拆分”而分片是前面所有拆分逻辑的统一调度框架。顺便提醒一句很多人把“分区卸载”“磁盘分区”这类系统和数据库分区混在一起。这完全是两码事。磁盘分区是操作系统层面的存储空间划分而数据库分区是表空间内部的逻辑存储组织。后面我会专门拿一节讲这个容易被混淆的地方。2. 分区最容易上手的第一步但别指望它救你于水火2.1 RANGE、HASH、LIST 分区的核心原理分区表在 MySQL 5.1 之后就有支持它的本质是在一张表内部把数据按规则分散到多个物理存储区分区段。查询时如果命中了分区条件优化器可以做“分区剪枝”Partition Pruning直接跳过无关分区只扫描目标分区。常见的三种分区方式RANGE 分区按连续区间划分典型是日期。比如PARTITION p2023 VALUES LESS THAN (TO_DAYS(2024-01-01))适合按时间归档、删除历史数据。HASH 分区按哈希值取模分布保证数据均匀。比如PARTITION BY HASH(user_id) PARTITIONS 8。适合没有明显范围特征、但需要均匀分布的字段。LIST 分区按枚举值列表划分。比如省市区、订单状态等离散字段PARTITION p_east VALUES IN (上海,江苏,浙江)。你可能会问那什么场景适合用分区以我的经验最典型的是日志表、流水表、订单表这类不断追加冷数据的场景。分区表最大的一个隐形好处是TRUNCATE PARTITION可以直接清掉一整段时间的数据比DELETE快几个数量级而且对热数据没有影响。听起来挺香对吧但它扛不住高并发写入和大容量扩容因为单表的分区数量普遍有上限比如某些版本推荐不超过1024个分区键如果没选好照样全表扫。2.2 分区键怎么选忘了这个分区就是自娱自乐选分区键是有讲究的。你选的分区字段必须满足两个条件查询条件里高频出现且分区条件能精确限定范围。比如订单表按“下单时间”做RANGE分区那所有业务查询最好都带时间范围条件。如果业务方天天按用户ID查最细粒度数据你按时间分区每次查询照样要遍历所有分区剪枝率几乎为零这就是典型的“分了个寂寞”。举个我实际见过的例子。之前有个支付流水表日增300万行90%查询都是靠“商户号交易日期”这两个条件。但是建表时只按日期做了RANGE分区商户号没有纳入分区规划。结果就是查某个商户某天的流水时虽然日期能剪到当天分区但这个分区里杂着所有商户的数据单分区物理文件依然很大全分区扫描依旧慢。后来加了“商户号HASH分区”作为二级规划把数据粒度控制在“天商户”级别查询才真正快起来。所以分区键选型一定要拿真实Query模式来反推而不是拿建数字典硬套。2.3 别把数据库分区和磁盘分区搞混了既然热词里大量出现“傲梅分区助手”“diskgenius分区工具”这类搜索词这里必须插一嘴。磁盘分区、硬盘扩容、分区助手这类操作解决的是操作系统存储设备的划分问题。比如你用傲梅分区助手给C盘扩容把D盘空余空间并给C盘这是在修改文件系统的卷布局。数据库分区则是数据库引擎内部的表空间管理机制它不关心底层磁盘分了几块。你可以把一张大的数据库分区表放在一块普通磁盘上的普通文件里也可以把不同分区放到不同物理磁盘上但这已经不是分区表本身决定的而是表空间的物理存储配置决定的。一个常见的操作误区是新人看到“分区表还有这么多数据怪不得磁盘满了”试图用“调整OS分区大小”来给数据库瘦身。方向完全错了。数据库表的大小要靠归档、清理、重建索引来控制OS磁盘不够用了是存储扩容的事和数据库内部分区没有直接关系。捞数据的和管存放容器的是两条线。3. 分表单机数据库的“第二增长曲线”3.1 垂直分表和水平分表先分清楚再动手分表是比分区更进一步的操作。它分为两种思路垂直分表按字段把一张宽表拆成多张窄表比如把“商品表”拆成“商品基本信息表”和“商品详情大字段表”。目的是降低单行宽度让每页能塞进更多行减少磁盘IO。水平分表按行拆比如订单表拆成order_0、order_1……每张表的结构完全一样只是数据范围不同。垂直分表适合那种“一行数据几百字节甚至上千字节但高频查询只关心其中几列”的表。最典型的场景就是“商品详情”这种大字段详情页看着介绍特别长但列表页只需要价格、标题、主图URL。把大字段拆出去列表查询的IO压力会小很多。水平分表大家听得最多也是“分片”的核心载体。它解决的瓶颈是单表数据量太大索引层数太深写入时B树频繁分裂查询时缓存命中率下降。但水平分表不是把表名改一改就完事关键是“查哪张表”这个路由规则。3.2 三道经典水平分表路由范围、哈希、映射表第一道是范围路由Range。比如按订单ID区间1~1000万放order_01000万~2000万放order_1。优点是实现简单扩容方便区间不够就加新表缺点是热点表问题非常严重——最新的数据永远落在最后一张表上而历史表完全闲着。第二道是哈希取模路由Hash Mod。比如shard_id order_id % 20把数据尽量均匀散到20张表里。优点是数据分布均衡写入竞争会被打散缺点是扩容极其痛苦从20张表扩到40张表时几乎每一条数据的归属都变了要做全量重排。这里能用的补救措施是“翻倍扩容”也就是从N个分片扩到2N个分片这样每个老分片只需要拆一半数据出去代价相对低一点。第三道是映射表路由Lookup Table。维护一张独立的“路由表”记录“数据ID→分表编号”。比如用户登录后查到user_id1001对应order_db_5.order_12后续查询全部走这个结果。优点是灵活、可控比如可以把大客户的所有数据故意放到独立的表缺点是每一次请求都要先查路由表有额外的网络开销和性能损耗而且路由表自身会变成高可用风险点。我个人的实践建议是新项目能用哈希取模尽量用哈希取模配合翻倍扩容策略综合性价比最高范围路由适合明确按时间归档、且接受冷热不均的场景映射表虽然灵活但一般只有租户隔离需求非常强的SaaS系统才值得用。3.3 一个冷热订单分表的实操案例拿订单表举例。假设你现在的订单主表已经突破8000万行日增20万行每天早上10点高峰期insert和select并存锁竞争明显。第一步先把订单表设计成“冷热分离水平分表”的组合order_hot_0、order_hot_1……共16张表存最近3个月的活跃订单用user_id % 16路由。每季度通过批处理任务把超过3个月且状态终态比如已完成、已取消的订单迁移到归档表order_archive_YYYY_MM按月份分表。第二步写个迁移脚本核心逻辑类似-- 伪SQL示意分批扫描需要归档的订单 INSERT INTO order_archive_202304 SELECT * FROM order_hot_0 WHERE create_time DATE_SUB(NOW(), INTERVAL 3 MONTH) AND status IN (FINISHED,CANCELLED) ORDER BY create_time LIMIT 5000;搭配应用层的双写删除删除时注意要一条条删避免大事务锁住热表。第三步查询层做好兜底。查询时先查热表没命中再查归档表或者提供“历史订单查询”入口由前端主动传时间范围后端直接把请求路由到归档库/归档表。就这么一套组合拳打下来热表基本能长期保持600万行以内高峰期写入和查询的矛盾大大缓解。4. 分库从单机走向分布式的关键一跳4.1 分库与读写分离不是一个段位的东西很多团队把读写分离和分库混为一谈。读写分离的本质是“多个副本分摊读请求”主库负责写、从库负责读数据最终一致或者强一致。但所有副本的数据总量是一样的任何单副本的数据量和写压力上限没有任何改变。分库不同。分库是把数据“切”成多份分别放在不同库实例里每个库只承担全量数据的一部分。比如用户库、订单库、支付库分开或者同一个业务库按用户维度水平拆成order_db_0、order_db_1……这才是真正的“分布式存储”。一般来说系统演进遵循这样的顺序单库单表 → 单库读写分离 → 分库分表 → 分布式中间件多活单元化。每一步都有明确的前提条件跳过某一步不是不行而是付出复杂度的代价会变大。4.2 分库以后的事务难题分布式事务没有银弹分库之后遇到的第一道墙是跨库事务。原来在一个库里的一个事务可以在多条表之间做ACID操作现在数据分布在多个库业务上一个“创建订单扣库存记账”操作可能涉及三个库的写入任何一个失败都要回滚这个保证怎么给分布式事务的常用路子我梳理了一遍XA两阶段提交数据库原生支持强一致性但性能损耗大协调者容易单点。业务量不大、对一致性要求极高的系统可以考虑但并发一高就很容易拖垮所有资源。TCCTry-Confirm-Cancel业务侵入性强每个操作要写Confirm和Cancel两个幂等逻辑。优点是灵活性高适合核心资金链路上的事务。Saga长事务把一个事务拆成多个有补偿动作的子事务正向走完失败就反向补偿。实现相对简单适合流程长、对中间状态可容忍的场景比如订单状态流转。本地消息表消息队列利用数据库表和MQ做最终一致性最常用。核心思想是“业务操作消息写入”在同一个本地事务里然后通过消息队列异步消费下游消费失败不断重试。我用得最顺手的还是“本地消息表MQ”方案。原因很简单大多数互联网核心业务不是实时强一致需求而是“最终一致对账兜底”。把复杂的分布式事务降级成“异步消息定时对账”稳定性反而更好。4.3 跨库Join要人命从源头规避而不是到处打补丁分库后第二个头疼的问题是“Join失效”。以前写一条SQL join两张表就完事现在两张表分属两个数据库除非走中间件做联邦查询效率极低否则基本只能靠应用层处理。我的几条实践经验按优先级排冗余字段。比如订单列表页需要显示用户昵称就把昵称冗余到订单表里下单时快照进去列表页查询直接取。应用层拼接。查出订单列表后再根据user_id集合批量查询用户库内存里做合并。性能可接受代码会多几行但绝对可控。汇总表/宽表。给高频查询建一张宽表用MQ异步同步各分库数据进去。适合数据密集型的统计类需求。尽量避免跨分片的关联查询。实在必须的设计阶段就把关联字段划到同一个分片键比如订单分片键和订单明细分片键都用order_id。一句话分库之后你写代码的惯性必须改掉不能再拿单库的思维写一条SQL搞定一切。拆分边界本质上是给数据建模方式画了一条新的线。5. 分片Sharding重量级的终极方案5.1 应用层分片 vs 中间件分片两条路怎么选分片是整个Sharding架构的核心调度层。落到技术上无外乎两种实现路径应用层分片在业务代码里封装一个分片路由组件自己算shard_id自己连对应数据源。优点是完全可控不依赖额外组件缺点是业务代码侵入重每换一个团队/项目路由逻辑都要重新Review。中间件分片用 ShardingSphere、MyCat、Vitess 这类框架把分片路由规则配置在中间件层应用层只访问逻辑库/逻辑表。优点是业务侵入小统一运维缺点是增加了一层网络转发和故障点调试时需要对代理层的配置有很深的理解。以ShardingSphere为例它与应用耦合的方式有两种一种是ShardingSphere-JDBC以Java客户端Jar包形式嵌入应用启动后直接改写SQL路由到目标库另一种是ShardingSphere-Proxy以独立服务方式运行对应用透明应用连接的还是“一个数据库”实际请求被转发到后端的多个分片。我在项目里倾向这样选如果是Java技术栈、团队愿意维护一定量的配置优先用ShardingSphere-JDBC性能开销更小排障更直接如果是多语言团队或者想屏蔽分片细节给非Java系统用那就用Proxy层。5.2 分片键选错全盘皆输分片键是整个分片设计的天花板。它一旦确定后续所有查询、扩容、数据迁移全都围绕它展开想改等于重做。我总结分片键选择的三条硬标准查询频率必须最高。比如订单系统的分片键选user_id还是order_id取决于你的核心查询是按用户找订单还是按订单号找详情。不能面面俱到只能保证最高频的查询能直接定位到分片。数据分布要足够均匀。取模后各个分片的数据量差异不能过大。举个例子如果按“省份”分片那广东一个省可能就占了全国数据的20%这种天然倾斜的字段不适合做分片键。不可变性要强。业务主键一旦建成就终身不改比如用户名改了没关系但user_id最好不要变。否则数据迁移和路由更新的成本会让你崩溃。带一点经验之谈有些团队喜欢用“user_id order_id”组合作为复合分片键先按user_id路由再在分片内部按order_id索引。这种模式在电商、金融项目里都很成熟但代价是“按订单号直接查”的场景会变成全分片广播查询。你要么给订单号设计反向索引表order_no → user_id要么接受广播查询的性能损耗二选一。5.3 分片之后SQL和索引的日常规则全变了分片不是把表拆了就完事它还改变了你写SQL的姿势。举几个最常见的改变全局唯一主键不能靠数据库自增。因为每个分片都有自己的自增起点撞ID是迟早的事。解决方案通常是雪花算法Snowflake或号段模式核心是生成趋势递增、全局唯一的ID。分页排序需要“查N份再归并”。例如ORDER BY create_time LIMIT 10, 20不能只查一个分片而是所有分片都查前30条然后内存归并排序再取第10到第20条。代价是“偏移量越大归并成本越高”。索引的设计范围变了。原来一张表里的联合索引可以随便建现在必须在每个分片内部生效无法建立跨分片的唯一索引只能靠“分片键表内唯一索引”双管齐下。这些变化会让一部分“SQL大师”非常难受——你会发现很多以前靠数据库能力解决的问题现在要拿到应用层来手动处理。6. 扩容与迁移没想好这一步先别急着分片6.1 翻倍扩容法成本高但最少坑的扩容路线分片后最怕的就是数据量超预期需要从16个分片扩到32个分片。如果当初路由规则是hash % 16现在要改成hash % 32原本一个分片的数据就要分裂到两个新分片。这个时候采用“翻倍扩容法”最稳把16个分片整体扩一倍到32个分片编号为0~31。原来0号分片的数据迁移目标就锁定在0号和16号原来1号分片迁到1号和17号以此类推。这样迁移时每个源分片的数据只需要对半分不需要跨其他分片调度任务可以并行跑。实际操作建议分三步第一步准备工作新分片建好表结构、初始化索引和数据校验基准。第二步开启“双写模式”新数据同时写旧分片和新分片旧分片作为兜底新分片做增量同步线上跑一段时间验证一致。第三步存量数据迁移按主键范围分批搬运历史数据搬运完成后做数据比对比对通过后将读流量逐步切到新分片最后关闭旧分片写入。这套流程走下来虽然要额外准备一批机器做过渡但出问题的概率比停机迁移低很多。项目有时间窗口的话值得这么做。6.2 一致性哈希让扩容不再“全军覆没”传统哈希取模的硬伤在于一旦分片数改变绝大多数数据的位置都会变化。一致性哈希Consistent Hashing就是为了解决这个问题出现的方案。一致性哈希把整个哈希值空间组织成一个环形结构每个分片节点在环上占据一个位置哈希值。数据通过hash(shard_key)落到环上然后顺时针找到最近的节点存放。当新增一个节点时只影响它相邻节点上的部分数据其他节点完全不受影响。它的升级版是“带虚拟节点的一致性哈希”。真实物理节点数量毕竟少容易在环上分布不均导致数据倾斜。解决办法是为每个物理节点生成几十上百个虚拟节点让它们在环上均匀排布数据分布会更平滑。比如原来3个Redis节点分布在环上现在增加1个只需要把相邻一段的数据重排其他2.5个节点的数据迁移量微乎其微。数据库分片也同样适用但要注意的是数据库分片的数据迁移成本比缓存高得多所以即便用一致性哈希也要配合合理的迁移工具和灰度切流。6.3 双写迁移 vs 停机迁移怎么选更实际关于数据迁移行业内两种主流方案我帮你把条件摆出来方案优点缺点合适场景停机迁移流程简单、一致性容易保证业务不可用时间长可接受维护窗口期的系统如内部管理后台双写迁移业务基本无感可灰度方案复杂需要处理回放和校验线上在线服务如电商、金融核心链路我在真实项目里通常采用折中方案凌晨低峰期先做存量拷贝拷贝完成后进入“双写校验”阶段白天的增量数据会同时写入新旧存储跑一整天后用对比工具做一致性校验第二天晚上再切读流量。整个过程业务侧几乎感觉不到变化。不要小看“校验”这一步。我见过太多脏事故就是因为迁移完没做数据比对上线后用户某些订单突然消失排查到凌晨才发现少迁了一批数据。所以无论时间多紧“抽查全量比对”必须做。7. 分片之后的技术债务提前认账免得后面拆东墙补西墙7.1 全局唯一ID雪花算法与号段模式的取舍分片之后数据库自增ID是废了你需要一套分布式ID生成方案。目前业界用得最多的两种雪花算法Snowflake一个64位long型ID1位符号位 41位时间戳 10位机器码 12位序列号。单机一毫秒能生成4096个ID全局趋势递增不依赖数据库。常见实现有美团的Leaf、百度的UidGenerator。号段模式从数据库统一取一段号比如每次取1000个应用本地发号用完再去数据库申请下一段。实现极为简单适合并发量不高的系统。如果并发量不大、团队图省事号段模式完全够用如果追求性能和无状态化雪花算法是更稳妥的选择。但注意雪花算法依赖机器码的全局唯一配置如果机器码配置重复跨机房环境竟然会产生重复ID这个上线前必须逐一核验。7.2 分布式Join基因法、字段冗余和查询合并前面讲了分库后Join要人命这里再补充两个消化手段。“基因法”是应对“通过子ID反查主ID”场景的巧妙思路。比如订单明细表的分片键也可以是order_id但你经常需要按user_id查某人的订单明细。做法是在生成订单明细时把user_id的一段二进制bits“注入”到order_id的低位。这样给定user_id你可以直接推出它对应的order_id模值从而路由到正确的分片反查时也不用全分片广播。这个方案需要在设计ID之初就定义好位数分配规则建议只在核心业务链路中使用因为它会牺牲一部分ID随机性。字段冗余就更直白了。比如订单列表页需要关联商品名称就直接把商品名称快照到订单行里下单时写入后面商品改名也不会影响历史订单展示。这一条操作能消灭90%的跨分片Join需求。7.3 深分页翻得越深越心痛分片之后的分页是一个隐藏很深的性能炸弹。单表查询LIMIT 100000, 20虽然慢但数据库至少做了优化分片环境下每个分片必须查出100000 20条然后在中间件层做归并把所有分片的100020条数据都抓出来再排序取20条。解决深分页问题的常用思路有几种限制页数深度比如最多翻100页把业务引导到“按时间范围查”。使用“基于游标的分页”比如WHERE create_time 上一页最后一条的create_time ORDER BY create_time DESC LIMIT 20不依赖偏移量翻页不会越翻越慢。对C端列表页干脆走“默认只展示最近N条数据 更早数据走异步导出”的产品策略从源头避免深分页发生。这不仅仅是技术问题也是产品设计问题。很多PM不知道深分页的成本如果后端不在方案评审时提出来后面上线慢就只能自己背锅。8. 用十几年踩坑经验总结什么情况千万别上分片8.1 先做容量体检别凭感觉拆库我这十几年见过最可惜的case是某创业公司上线才一年数据量总共两千万行技术负责人一激动就上了MyCat分片。结果微服务、分片、分布式事务全上业务迭代到第四个月就卡在跨分片查询上最后花了两个礼拜把分片全部回滚退回单库。在你定方案之前请先认真做一次数据评估单表行数是否已过千万且查询持续变慢单实例QPS是否持续超过5000磁盘、CPU是否存在长期高水位存储容量是否逼近物理上限短期半年内业务量是否有持续翻倍的预期团队是否有能力维护分片中间件和分布式数据一致性如果这几个问题大多数都不成立那最优解其实是“优化索引归档冷数据升配”。分片是不是好东西是但它更像一把手术刀不是日常保健品。8.2 拆到哪一步取决于你的业务特征哪怕真的需要分片也不是一上来就要分库分表搞全套。我的建议是阶梯式演进数据量大但查询维度集中优先分区。单表数据超过千万、且高频写入集中优先垂直分表 归档 水平分表。单实例连接数告警 / 容量瓶颈再走上分库。数据量持续增长、对扩容有明确预期才需要引入完整的分片框架并把“扩缩容”当作基础能力从第一天就设计好。这个顺序不是懒而是每往前走一步系统复杂度、运维成本、故障半径都会翻倍。合理控制复杂度本身就是一种高级的技术决策。8.3 最后送一份避坑清单分片键不要选天然倾斜的字段性别、省份、冷热悬殊的类型。分片数量最好选2的N次方便于未来翻倍扩容。不要为了一个低频SQL做广播查询缓存它或者加一张冗余表。全局自增ID不要留在分片里全部统一上发号器。每年至少做一次“存量数据梳理”该归档的归档该裁剪的裁剪。中间件版本尽量跟随社区稳定版本不要停留在老版本不升级分片路由的bug往往只在新版修复。我在实际项目里反复验证过一件事凡是老老实实做完容量评估、选择合适拆分的系统后面都走得比较顺凡是脑子一热要“一步到位”的最后都在还架构债。数据库分片的本质不是帮系统无限变强而是让系统在你可控制的复杂度内持续演进。希望你拆之前想清楚拆之后能控制住。