ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

MySQL分库分表实战:分片键选型与迁移避坑指南

MySQL分库分表实战:分片键选型与迁移避坑指南 做 MySQL 核心存储的团队只要数据量和并发一起来几乎都会遇到同一个灵魂拷问到底要不要上分库分表如果要上是垂直分库还是水平分表分片键选谁才不会踩雷这些坑我都在生产环境里踩过。今天这篇就按自己的复盘经验把分库分表路上最容易翻车的地方摊开讲透什么时候该拆、三种拆分方式怎么选、分片键怎么定、迁移分几步走以及上线之后最常见的几个深坑怎么排查。适合正在评估拆分方案、或者已经被线上大表压得喘不过气的开发同学也适合准备做数据库架构升级的团队当参考。1. 先别急着拆分库分表是一笔“高成本负债”1.1 什么信号出现才该动手分库分表和加索引、调SQL不一样它不是一项“性能优化手段”而是一次存储架构的重新设计。一旦上车SQL写法要改、事务边界要改、查询习惯要改连运维方式都要跟着变。所以我的第一个建议是先用门槛挡住绝大多数不该拆的场景。什么时候才真正需要考虑拆分我从实际运维中总结了四个硬信号至少命中两三个再动手单表行数超过千万级别且持续增长索引层级变深写入和查询都明显退化数据库连接数长期打满应用侧不断报Too many connections加连接数上限也没用高峰期 QPS 或 TPS 已经逼近单实例硬件上限CPU 常年 70% 以上SQL 优化和参数调优都试过了写入容量成为瓶颈比如订单、日志、流水这类只增不改的数据单库磁盘和写入速度都跟不上业务增速。这里有个容易误判的地方单表 500 万行、偶尔慢查询不一定需要分表。很多慢查询纯粹是索引没建对或者 SQL 写法本身有问题。我见过一个项目一张表两千万数据业务方嚷嚷着要拆表结果我一看日志大部分慢查询都是因为LIKE %xxx%这种无法走索引的写法。把查询改写加联合索引之后性能直接翻了十倍分表需求自然消失了。1.2 常见的两种误判缓存能解决的事别用分库分表扛除非数据库已经到了物理极限否则优先考虑其他手段。这里说两个我反复跟团队强调的“不要把分库分表当万金油”的场景。第一是读多写少的场景。如果 90% 流量是查询先上缓存。Redis 或本地缓存可以扛掉绝大部分读压力数据库压力会大幅下降。分库分表解决的是“存储容量”和“写入吞吐”的问题不是“傻读”的问题。你用分库分表去顶读流量相当于雇了一整支装修队来换一个灯泡。第二是历史数据归档没做。很多业务表 90% 的数据是三年以上的冷数据平时根本不会被查询命中。这种场景正确的做法是先做冷热分离——把半年或一年前的数据迁到归档表、归档库甚至直接放到数仓或对象存储里。我拆过一个用户操作日志表拆完发现其中 80% 是两年前的过期日志早该归档了白白为这些冷数据花了大量分库分表的成本。1.3 从单库到分布式存储的整体演进路线分库分表不是一步到位的业界踩了这么多年坑基本沉淀出一条比较稳妥的演进路径先做 SQL 优化和索引优化把能省的资源都省下来引入缓存层解决热点读引入读写分离主库写、从库读分摊读压力做垂直分库按业务域把不同模块拆到不同库做垂直分表把单个大表的热点字段和冷字段拆开最后才做水平分表解决单表数据量过大和单库写入瓶颈的问题。这个顺序本质上是按照“成本从低到高、收益从明显到隐蔽”来排的。前面几步做完很多系统已经能扛住相当高的并发根本走不到最后一步。反过来如果跳过前几步直接上水平分表你会发现拆完之后 SQL 不能随便 join 了、事务要改分布式事务了而数据库本身的问题比如一条烂 SQL 没优化依然存在只是被你拆碎了而已。2. 垂直分库、垂直分表、水平分表三种拆分一次讲清2.1 垂直分库按业务域把不同的表拆到不同的库垂直分库的核心思路是“按业务模块切分数据库”。比如一个电商系统用户、订单、商品原来在同一个库mall_db里现在拆成user_db、order_db、product_db三个库分别归属用户中心、订单中心、商品中心。这种拆分通常伴随微服务化进行订单服务的库只允许订单服务访问其他服务要想拿数据必须通过接口调取而不是直接连库。所以垂直分库解决的不仅是单库容量和连接数压力更重要的是划清了数据边界避免一个库塞满所有业务表、互相争抢连接和 IO。垂直分库的难点不在技术而在组织架构。如果你们的服务还是单体应用拆了库之后代码还是全部写在一起那么每次查询都要跨库、每个事务都要跨库复杂度会陡增。我的建议是垂直分库必须跟在服务拆分后面走业务边界没理清楚之前不要动库。2.2 垂直分表把一张大表的字段拆成两张或多张表垂直分表是把一张列很多的表按照字段的访问频率和关联度拆成多张表。最常见的是“主表 扩展表”模式。举个例子用户表user有 30 个字段其中id、name、mobile、status这类字段每次查询都要用到是热数据而avatar、signature、last_login_ip、register_source这类字段很少用但是占存储空间是冷数据。那就拆成user_main和user_extend两张表通过user_id关联。这里有个细节容易被忽略垂直分表拆的是“行宽度”它不能解决单表行数过多的问题。想象一张表是 1000 万行、每行 2KB另一张表同样是 1000 万行、每行 100 字节前者的扫描成本远高于后者。垂直分表就是通过“瘦身”让单页能容纳更多行从而降低 IO 和缓存淘汰率。但对于行数爆炸导致的写入瓶颈它无能为力。2.3 水平分表按行拆分才是解决“大表”的根本手段水平分表是把同一张表的行数据按照某个字段的取值分散到多张结构完全相同的子表中。比如订单表按user_id模 16拆成order_0到order_15共 16 张表。水平分表的意义在于单表行数被控制在可接受范围内索引体积减小B树层级变浅写入和查询都能保持稳定。同时如果把不同子表放到不同库的不同实例上还能同时摊薄存储和计算压力。这也是标题里“水平分表”和“垂直分库”并列出现的原因——它们一个解决“表太大”一个解决“库太杂”是不同维度的问题实践中最优方案往往是两者组合。2.4 三种拆分方式对比速查表对比维度垂直分库垂直分表水平分表拆分维度按业务模块按字段冷热按行数据范围/哈希解决的核心问题连接数、单库容量、业务边界单表行宽、IO浪费单表行数多、写入瓶颈拆分对象库与表单张宽表单张超大数据量表对应用透明性需改造访问方式需改造SQL需引入路由层典型代价分布式事务、接口调用需要额外一次查询跨分片查询、扩容麻烦适合场景微服务架构下的多业务域字段多、有冷热之分的大宽表日志、订单、流水等持续增长的表一句话总结垂直是“分家”把不同类型的东西分开水平是“分灶”把同一类的东西均匀分配到多个锅里。两者不冲突真正的生产架构里经常先用垂直分库把服务边界划开再对核心大表做水平分表最后配合垂直分表把宽表瘦身。3. 分片键选型决定成败的关键决策3.1 为什么说分片键是“第一重要决策”分片键Sharding Key是水平分表时用来决定某一行数据应该落在哪张子表的字段。它决定了路由的效率和查询的方式选错后面所有环节都会跟着变形。我见过最典型的翻车案例某团队订单表按order_id做哈希分片结果业务上大量查询是“查某个用户最近的订单”。查询条件里没有order_id只有user_id于是每次都要把 16 张子表全扫一遍再合并。分表之后查询反而变慢了团队一度想要回滚。这里本质问题在于路由规则只认识order_id但业务高频查询不认识order_id。数据被均匀分散到了各个分片却无法从查询条件定位到具体分片分布式架构最核心的“数据本地性”优势直接作废。3.2 分片键的五条选择标准根据实际经验一个合格的分片键应该同时满足以下条件查询覆盖率高高频查询条件里必须带上这个字段最好能直接定位到一个分片区分度足够高字段取值足够分散不能像status只有几个可选值否则某个分片会堆积大量数据稳定不可变字段值一旦生成就不会修改。否则用户更换手机号后原来按手机号分片的数据定位全部失效产生方式有规律ID 类字段要有全局唯一性且最好由应用层或者发号器生成而不是依赖数据库自增主键访问均匀、无热点字段的取值分布要均匀尽量避免少数值贡献了绝大多数流量。实际项目里不会有一个字段完美命中所有标准但当业务不止一个高频查询维度时就要根据业务权重做取舍。电商的订单表就是最典型的例子买家会高频查“我的订单”卖家会高频查“店铺订单”运营还会按订单号精查。三个维度互相冲突不可能同时作为分片键。这时候要回到业务本质看哪个维度流量最大、SLA要求最高舍弃部分查询的可路由性用“基因法”或者数据冗余来补偿。3.3 三种主流路由方案哈希取模、范围分片、一致性哈希分片键定好之后还要选路由算法。路由算法决定了“按分片键的值计算出数据落在哪张子表”的具体规则。哈希取模是最简单直接、也是最常用的方案。分片键的值经过哈希函数映射后对分片总数取模。比如总分成 16 片hash(user_id) % 16结果落在 0 到 15 之间对应table_0到table_15。# 伪代码应用层路由逻辑 def route(user_id: int, db_num: int, table_num: int): total db_num * table_num idx hash(user_id) % total # 逻辑分片号 db idx // table_num # 物理库编号 table idx % table_num # 物理表编号 return fdb_{db}.table_{table}哈希取模的优点是人话能听懂的简单数据分布也均匀。最大缺点是分片数一旦确定后续扩容极其痛苦。从 16 片扩成 32 片绝大多数数据的路由结果都会变化意味着存量数据要大面积迁移。如果没有一套完整迁移方案扩容就是一次小规模“换库手术”。范围分片是按分片键的值区间划分比如订单号 1 到 1000 万放在table_01000 万到 2000 万放在table_1。这种方案天然便于范围查询扩容时只需要新增一片不用动旧数据。缺点是数据分布可能严重倾斜月初订单少月底订单多热点全压在一张表上。所以范围分片比较适合时间序列数据比如日志、流水不适合高并发订单。一致性哈希是为了解决“增加/减少节点时迁移量过大”而设计的。它把哈希值空间组织成一个环形结构每个分片负责环上一段区间数据落到哪个分片由它在环上的位置决定。节点增减时只有相邻分片的数据需要迁移。为了均衡分布一致性哈希还引入“虚拟节点”概念让每个物理节点在环上拥有多个位置。但一致性哈希的实现复杂度高、排查路由相对困难MySQL 分库分表里用好的人远远少于用哈希取模的。三种方案我用一张表收一下路由方案数据分布特点扩容代价实现复杂度适用场景哈希取模均匀高需迁移大量数据低大多数在线业务范围分片易倾斜低只需新增分片低日志、时序流水一致性哈希较均匀中只迁移相邻数据高节点频繁变化的分布式系统3.4 分片键选错后怎么补救如果上线后发现分片键选错了不要慌有几个止损手段。小规模业务可以用“索引表”兜底单独建一张映射表保存分片键和行 id 的对应关系。比如订单表按user_id分片但运营经常用order_id查单那就维护一张order_user_index表通过order_id查出user_id再定位到具体分片。代价是每次精查多一次索引表查询但至少能保证功能可用。另一种思路是“基因法”。设计主键时把分片键的某些位种进主键里。比如order_id的末尾 4 位取自user_id的末尾 4 位这样拿到order_id就能直接算出user_id对应的分片号不需要额外查询。这个方案很棒但是要求生成 ID 的规则从一开始就设计好中途很难改。最实在的提醒是分片键的选型必须在拆分立项阶段就拉上所有业务方把高频查询 SQL 全部列出来逐一确认哪些查询必须路由到单分片、哪些可以接受跨分片聚合。这一步花一两天时间能省掉后面半年的返工。4. 从单库到分库分表的落地实操四步迁移法4.1 第一步容量预估与分片数规划动手迁移前第一件事是计算分片数量。分片数既不能太少又不宜过壕我的经验公式是按未来三年的业务增长预估数据总量再除以单表行数目标上限。假设当前订单表 8000 万行年增速约 50%三年后总量大约 2.7 亿行。单表行数控制在 500 万到 800 万行以内是相对舒适的状态取 800 万作为阈值那么分片总数 2.7 亿 / 800 万 ≈ 34 片。为了避免将来扩容我会直接取 2 的幂次方物理上分成 64 片。为什么取 2 的幂次因为分片数从 64 扩到 128 时只需要把原分片编号的最高位从 0 变 1迁移量恰好是一半数据可以用二进制位运算轻松做平滑扩容。这是老 DBA 都懂的“二次扩容”技巧。分片键如果选择user_id那么路由逻辑就是hash(user_id) % 64。为了分摊写压力64 张表不要全放在一个实例里建议按 4 库 × 16 表 或者 8 库 × 8 表 布局。库和表的组合方式会影响后面的连接数分配和运维复杂度建议单独画一张拓扑图标注清楚每个物理库上有哪些表、通过哪些分片范围路由。4.2 第二步全局唯一 ID 生成方案先行分库分表之后数据库自增主键不能再用了——因为每个分片都有自己的自增起点多张表生成的 ID 会重复全局唯一性无法保证。所以分片键如果是主键必须在迁移前上线全局 ID 生成器。业界用得最多的是雪花算法Snowflake ID。一个 64 位的长整型 ID由时间戳、机器编号、序列号组成。它生成的 ID 全局唯一、趋势递增、且不依赖数据库。我建议组合使用应用内嵌雪花算法生成器避免每次生成 ID 都请求外部服务减少网络开销对于需要严格顺序的业务可以借助数据库发号器用一个独立的id_generator表维护多段步长来避免时钟回拨问题。这里补一个我踩过的坑雪花算法强依赖机器时钟如果服务器做了 NTP 时间同步偶尔会出现时钟回拨导致生成的 ID 可能重复。解决方法是发号器内部保留上一次生成 ID 时的毫秒时间戳一旦发现当前时间小于上次时间就在内存里阻塞等待时钟追上或者直接为本次生成微调偏移量。这段逻辑不复杂但不能省。4.3 第三步双写迁移 历史数据全量同步生产环境做分库分表迁移绝对不能“停服一刀切”。业界最稳的套路是“双写 全量 对账 切读”。双写的意思是应用侧在写入老库的同时把同一份数据也写入分库分表后的新库。代码层面可以用 MQ 异步双写也可以用 AOP 加一个双写切面走一个开关控制。双写阶段新库的写入路径就是不间断接受流量验证发现路由 bug 或者写入失败马上关掉双写开关业务无感。全量迁移是指把存量数据从老库导入新库。这里推荐用工具批量做比如mysqldump按主键范围分批导出再导入到分布式表或者用DataX、Canel等数据同步工具。必须按主键或者时间游标分批处理避免一次性加载几千万行把数据库 IO 打满。整个过程我建议用流水账记录每批迁移多少行、耗时多久、是否有报错、对账结果如何。全量迁完之后老库可能又新增了一批增量数据所以还要有一个增量追平的过程。最简单的方式是在迁移期间持续消费 binlog把老库的增量变更同步到新库直到两边数据完全追平。4.4 第四步灰度切流与快速回滚数据追平之后开始灰度切流。不要一次把全部流量切到新库按用户灰度比如先切 1% 的用户观察指标再逐步放大到 10%、50%、100%。切流阶段最需要注意两个指标一是写入新库的失败率二是分布式查询的耗时和报错率。我见过团队把流量切到 50% 之后发现某些跨分片查询把数据库连接池打爆了因为旧库的查询是一条 SQL 搞定新库要并发查 16 张表再合并结果单位查询的资源消耗翻了不止一倍。这时候要回头优化查询逻辑把“并发查 16 张子表”改成“按业务维度路由到 1 张表”或者增加结果合并层的聚合能力才能继续放量。灰度环境里一定要准备一键回滚开关。万一新库出了问题把开关拨回去流量回落到老库比在代码里紧急改 bug 快得多。这个开关最好在架构设计阶段就预留好而不等出问题当天再开发。4.5 实操过程中的数据校验与对账技巧迁移过程中我强烈建议做三层对账行数对账老库和新库每个分片的行数总和要一致抽样字段对账按主键抽样比对关键字段的值是否完全一致指标对账统计最近时间段的写入量、查询量看两边是否一致。对账不是一次性的双写期间要持续跑。我习惯把对账结果输出到一张监控表设置阈值告警任何不一致立刻告警、立刻冻结双写防止脏数据继续蔓延。等稳态运行一两周之后再考虑把老库降级为只读备份库最终归档下线。5. 拆分之后躲不开的四大深坑与排查实录5.1 跨分片 Join不能再用一条 SQL 解决分库分表之后跨分片 Join 基本不可行。两个表如果分片键不一致数据分散在不同的物理库上SQL 层面无法做 Join应用层做内存 Join 的成本又高得离谱。我的处理原则是能下沉到数据冗余的绝不在应用层拼。比如订单表需要展示商品名称那就把商品名称冗余到订单表里作为冗余字段需要展示买家昵称就冗余买家昵称。用空间换 Join这是最直接有效的手段。实在需要实时聚合的可以上异构同步方案用 Canal 订阅 MySQL binlog把数据同步到 Elasticsearch 或者 ClickHouse由这些引擎承担复杂的聚合查询。这个方案我测下来非常稳但要注意同步链路存在秒级延迟不适合强一致数据场景。5.2 跨分片分布式事务别再迷信强一致性跨分片的事务想要保持 ACID必须引入分布式事务方案。常见的有两阶段提交XA、TCC、本地消息表、消息队列最终一致性。我的实际经验是业务侧尽可能避免跨分片事务把事务边界收敛到一个分片内。具体怎么收敛在设计分片键阶段就要尽量让一个事务涉及的数据落在同一个分片。比如“下单减库存”如果订单表和库存表分片键不一样那一次事务就跨了两个分片如果都按商家 ID 分片同一商家的订单和库存天然落在同一个分片事务就还是本地事务。如果实在无法避免推荐用“本地消息表 MQ”的方案落地最终一致性不要强行上 XA。XA 在低并发下看起来很美一旦分片数增加、事务参与方变多性能损耗会非常明显线上很容易出现“锁等待超时”和“连接吃紧”的问题。5.3 数据倾斜哈希取模也不代表绝对均匀很多人以为用了哈希取模就万事大吉其实哈希只是一种概率均匀。当分片键的取值空间本身就倾斜时比如某个大客户的订单量占据全站 30%那它的所有数据都会被路由到固定的几个分片形成“热点分片”。这个热点分片的磁盘 IO 和查询压力远超其他分片数据库整体就变成了“木桶效应”。我处理过的最典型的案例一个 toB 系统按merchant_id分片某头部商户的订单量是普通商户的上百倍负责它的那张表被拖垮直接影响全站。后来采用的方案是“分片键 业务后缀”对超大商户在分片键后面拼接一个 0 到 15 的随机后缀让同一个商户的数据也能打散到多张表。查询时带后缀批量并行查再合并结果。代价是代码复杂度上去了但能有效救活热点分片。5.4 扩容迁移数字上的“扩容”远比想象中麻烦最后聊一个所有分库分表系统迟早要面对的扩容。哈希取模分 16 片用了三年数据量涨了要扩到 32 片。这时候的问题不是“加几张表”这么简单而是存量数据的大量路由结果会变必须在扩容方案里做数据迁移。平滑扩容最基础的方法是“停机扩容”把服务暂停跑数据迁移验证后再启动。业务允许停机的话这是最简单、最可控的方式。不可停机的系统可以用“新旧双路由”方案应用层同时维护新旧两套路由规则写数据时双写查到旧分片的数据时从新分片重建并迁移。等数据追平切到新路由下线旧路由。整个过程非常考验团队的运维能力所以再次强调分片数规划一定要留足余量别一开始就规划得刚刚好。写在最后的一些真实体会分库分表这个方案真的是“拆之前百般犹豫拆之后百般谨慎”。我自己的感觉是技术上最难的往往不是选型或者写路由代码而是业务查询维度的梳理和迁移过程中的耐心。很多团队倒在了迁移的半路上不是因为技术不行而是因为没有做好充分的灰度验证和数据对账就急于放量。如果这篇文章只能给你留下一个记忆点我希望是这句话分片键选型是分库分表里唯一一个“一开始错后面全盘错”的决策点规划时间至少要占到整个项目的一半。再实际的建议是团队在真正动手之前先用模拟数据把路由规则、扩容方案、迁移演练都跑一遍把可能踩的坑尽可能提前暴露在测试环境。最后补一句分库分表从来不是数据库问题的终点。真正稳定的大规模系统往往是分库分表、缓存、归档、异构查询、消息队列共同协作的结果单一手段解决不了所有问题。你们团队现在遇到的大表难题属于哪一类欢迎带具体场景来评论区聊我会挑典型的出来拆解分析。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表