ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

分库分表实战指南:从拆不拆到怎么拆的完整避坑方案

分库分表实战指南:从拆不拆到怎么拆的完整避坑方案 很多团队的数据库分库分表是在一次线上告警之后才匆忙开始的。慢查询堆积、磁盘IO拉满、主库连接数告急DBA半夜拉群问要不要拆库。我参与过不下十次这样的紧急评估也亲手把一个日订单百万级的系统从单库拆到32库64表。这一路踩过的坑告诉我分库分表真正的难点从来不在“怎么拆”而在“要不要拆”和“拆完之后怎么活”。这篇文章不聊概念只聊实战——什么信号下必须拆、四种拆分怎么选、分片键怎么定、迁移怎么做、拆完之后的坑怎么填。无论你现在是正在做容量评估还是已经在分片集群里被跨库查询折磨这些场景大概率你都会遇到。1. 先判断是不是真需要分库分表三个误判与三个有效信号1.1 最容易踩的误判把数据量大直接等同于必须分片先说结论数据量大不是分库分表的充分条件。MySQL InnoDB单表存几千万行很常见只要索引命中、写入可控很多业务跑五六年都不会碰到本质瓶颈。我见过一张物流轨迹表单表超过一亿行、日增两百万但因为它只有两个查询维度且索引完全覆盖靠着归档和分区表一直撑得很好。数据量增长真正带来的问题是慢查询的放大效应全表扫描成本随行数线性上升二级索引过大又导致每次写入都要维护多棵B树随机IO明显增加。所以第一步永远不是设计方案而是先把慢查询治理了、把该加的索引加上、把该归档的数据归档掉。我几乎在每个团队都见过以下三种误判误判一用“表体积”代替“瓶颈点”。表占了多少GB不代表必须分片关键看单条SQL的响应时间以及CPU、IO、连接池的水位。一个10GB的表如果查询都是走主键可能比一个1GB但索引全失效的表健康得多。误判二用峰值偶发故障代替持续瓶颈。大促带来的十分钟峰值可以通过限流、队列、缓存扛过去没必要为了一次促销就背上永久的分片复杂度。分片是持久性架构决策不是应急手段。误判三把索引优化失败当作分片依据。索引没建对导致的慢查询分片之后只会更慢因为跨片聚合的成本远高于单表扫描。先找DBA看执行计划而不是先找架构师定方案。1.2 真正触发分库分表的三个有效信号写入吞吐超过单库上限单主库的稳定写入一般在每秒两三千到上万事务之间取决于SQL复杂度和硬件配置。当业务峰值需要每秒数万写入而读写分离、批量合并、异步削峰都试过之后仍然打不上去水平拆分就是唯一出路。这个信号在IoT设备数据上报、交易流水、消息记录场景里非常典型。单库连接数被打满连接池上限、MySQL的thread_cache、文件描述符都有限度。我遇到过数据量并不大、但几百个微服务实例共用一批连接的场景每个实例池里留20个连接瞬间就能把数据库的连接数吃光。这种瓶颈分表解决不了必须分库。行锁竞争和索引膨胀热点行更新导致锁等待飙升比如秒杀场景对同一个库存行的并发update或者一张宽表有十几个二级索引每次insert都要同步维护十几棵索引树。这两个问题往往是“读写混合”场景特有的也是压测时最容易暴露的。提示判断要不要拆最靠谱的方式是压测。用全链路压测把流量打到两倍峰值观察数据库的CPU、连接数、锁等待和慢查询四个指标比任何经验公式都准确。我见过太多拍脑袋定了32库最后实际只用8库就够了的案例。2. 四种拆分方式怎么选垂直拆、水平拆、分库、分表2.1 垂直拆分先按业务边界切别急着按行切垂直拆分的本质是把一个“大而全”的库拆成多个“小而专”的库比如把用户、订单、商品、支付拆成独立库。它解决的是模块间的相互干扰一个后台报表查询把IO打满会拖垮所有线上订单接口。这种拆分是成本最低、风险最小的方案也是我在绝大多数系统里推荐的第一步。垂直拆分要注意一个边界不能拆完之后业务join满天飞。拆分前先做领域梳理把真正高耦合的表留在同一个库里。比如订单表和订单明细表必须同库但订单表和用户表可以拆开通过冗余用户快照或RPC组装数据。判断标准很简单如果两个表的关联查询是核心链路的一部分就不要试图用跨库join解决先把它们放在一起。2.2 水平拆分数据行按规则散列到多个分片水平拆分解决的是单表数据量或单库吞吐的问题。核心是选一个分片键把行数据均匀打散。这里最常犯的错是把“均匀”等同于“随机”其实更重要的是业务查询的维度——分片键必须是你最高频、最刚需的查询条件。比如订单库按 buyer_id 分片那么“查某用户的订单列表”就是单分片查询如果按 order_id 分片这个查询就要广播到所有分片那个性能差距是数量级的。水平拆分的数据分布有两种模式一种是连续区间比如按用户ID范围前1000万分到分片0、1000到2000万分到分片1另一种是离散散列比如取模或哈希。连续区间的优点是范围扫描友好缺点是容易倾斜新用户集中在尾部离散散列分布均匀但范围查询和排序基本做不了。工程上离散散列更主流因为单点倾斜的代价比范围查询的便利大得多。2.3 分库、分表怎么组合分库解决连接和IO瓶颈分表解决单表行数膨胀两者可以独立也可以叠加。我见过不少团队只分表不分库结果单表数据量是降下来了但所有表还在同一个实例上连接数和总IO瓶颈一点没解决。反过来只分库不分表单表两亿行照样把查询拖垮。下面这个对照表是我做方案评审时最常用的拆分方案解决的核心瓶颈典型场景代价单库分表单表数据量过大导致的查询/写入变慢日志流水、订单明细同一实例内维护多张物理表事务不受影响分库不分表连接数、IO、CPU等单库资源瓶颈高并发写入、多业务模块并存跨库事务与跨库join基本消失分库分表数据量与并发双高头部互联网订单、消息系统复杂度最高所有难题叠加实际选型时我的经验是连接数先打满就分库单表查询先变慢就分表两个都到了就一起做。别为了“一步到位”直接上最高复杂度架构是演化出来的不是设计出来的。3. 分片键与路由算法成败在此一举3.1 分片键选错的现场分片键选错是分库分表唯一没有补救机会的失误。拆完之后想换分片键基本上等于全量数据重分布业务要停很久。举个例子如果按订单号分片但业务里大量查询是“查某个用户的所有订单”那每次查询都要广播到所有分片32个分片就是32次查询再聚合更严重的是如果分片键本身不是业务的稳定属性——比如用手机号分片但用户可能换绑——就会导致路由错乱和数据搬迁。选分片键有三个硬性要求缺一不可基数足够大区分度低会导致数据集中。用状态字段如订单状态只有几个值分片等于把数据压到几个分片上。分布足够均匀按用户ID、订单号这类自增或随机值通常是好的按城市、渠道这类枚举值很容易倾斜。尽量不可变分片键一旦作为路由依据业务上就不能再改这个字段否则数据归属直接错乱。满足这些条件之后还要过最后一关这个键要能覆盖80%以上的核心查询。如果做不到要么接受广播查询要么增加二级索引映射表比如维护一张user_id到order_id的索引表但这是额外成本能不做就不做。3.2 三种路由策略的对比路由策略决定了数据怎么落到分片上也决定了未来扩容的代价。主流方案就三种取模、范围、一致性哈希。取模是最直观的shard shard_key % 分片数实现简单、定位精准但扩容时数据要全量重分布。8库扩16库还好可以用双倍映射减少迁移量旧数据只迁移一半如果是8库扩10库这种非倍数扩容几乎每个分片都要搬家。按时间/ID范围分片适合日志和冷热分离场景比如按月建表、按季度建库。优点是天热归档方便直接把整个分片下线就行缺点是当前写入集中在最新分片天然有热点而且各分片数据量可能严重不均。一致性哈希是理论上最优的扩容方案数据迁移只影响相邻节点。但实际工程里中间件支持不成熟、路由计算复杂、运维排障成本高我很少见团队在生产环境直接用纯一致性哈希。更实用的替代方案是“虚拟分片位”也就是常说的预分片提前把数据空间切分成4096个逻辑分片再把这4096个逻辑分片映射到当前物理分片。扩容时只需要调整映射关系搬走部分逻辑分片路由逻辑不变。路由策略数据分布扩容代价适用场景取模均匀高需重分布数据量稳定、分片数确定范围可能倾斜低只影响新区间日志、时序、冷热分离一致性哈希较均匀低只迁移相邻节点节点频繁变化的场景预分片虚拟位均匀低调整映射即可中长期扩容规划3.3 热点与倾斜处理即使分片键选得好热点也无法完全避免。比如大促时某个超级用户的订单量暴涨他的数据集中在某一个分片上该分片IO飙升。处理热点的思路有三层第一层是缓存前置把热点读挡在缓存层第二层是分片键组合化比如订单表除了buyer_id还可以在分片键里拼上订单日期让单个用户的数据也能散到多个分片上第三层是接受局部热点把热点分片的规格提升或者在中间件层对热点分片做读写分离。第一层最便宜第三层最现实。4. 从单库到分片一套可灰度、可回滚的迁移方案4.1 分片数怎么定容量评估与目标行数迁移之前先定分片数这不是拍脑袋而是算出来的。先估算五年的数据总量日增行数乘以每行平均字节数再乘以1800天左右加上已有数据量。然后定单分片目标行数我一般控制在2000万行以内这是MySQL InnoDB在普通索引SQL下还能保持良好响应的大致阈值。分片数用总行数除以单分片目标行数向上取整到2的幂。举个例子一张订单表日增50万行每行约500字节五年总量大约是50万×1800×500字节算下来约45亿行。按单分片2000万行需要225个分片向上取整到256。这时候要考虑清楚256个分片的元数据管理、连接管理、备份恢复复杂度都不低如果读写比不高、并发上不来也可以先做128个分片留一倍扩展空间。这里我特别强调分片数宁多勿少因为后续扩容的代价远高于初期多开几个分片的成本。4.2 双写与数据回放迁移的正确姿势老库到新分片集群的数据迁移我做过的成功方案基本都长这样先全量导出历史数据导入新集群同时用binlog订阅增量变更回放到新集群最后切流量前做一次数据对账。全量导出的SQL要分批limit拉取避免一次性大事务把源库拖死导入新集群时要关闭唯一索引校验以外的所有约束并且按分片并行导入。双写是更稳妥的补数方式。在代码层同时写老库和新库顺序是先写新、再写老以老库为数据准绳读请求先读新库失败或校验不一致则回退老库。双写期一般在两到四周长短取决于业务容忍度和对账脚本的完善程度。这期间老库仍然承担全部写流量新库只接收复制流量所以新库的写入压力很低不太会出现性能问题。对账脚本是整个迁移的定心丸。不能只比对行数要比对关键字段的哈希值比如对主键排序后拼接所有业务字段做MD5分段比对。我见过行数一致但某几个字段被截断的案例只数行数根本发现不了。4.3 灰度切换与回滚预案数据追平之后流量切换不能一步到位。按流量百分比灰度通常的节奏是5%、20%、50%、100%每档观察至少半天到一天重点盯支付、下单这类核心链路的错误率和慢查询。切换依据是流量网关或注册中心的路由规则而不是改代码发版。回滚预案要在切流量之前就写好而不是出问题再想。最有效的回滚就是“把流量切回老库”前提是老库链路全程保留、双写不中断。灰度期间如果发现新库数据缺失或路由错误直接切回老库代价仅仅是丢了双写期间的部分新库数据老库数据是完整的。一旦全量切换完成并稳定运行两到四周老库才可以降级为只读备份库这时候才算真正迁移完成。5. 分片之后的四大难题跨片查询、分布式事务、全局主键、扩容5.1 跨分片查询能避免就避免分片之后最直接的冲击是SQL能力退化了。原来一条带join的查询中间件要么不支持要么需要在应用层做多分片并发查询再组装。我的原则很明确分片之后禁止跨分片join和跨分片事务业务代码要为这个约束重构。应对跨片查询的常见手段有三种。第一是冗余宽表比如订单分片了但后台需要按商家维度统计那就单独维护一张按商家分片的统计表源数据变更时异步同步过去用空间换查询便利。第二是异构存储把需要复杂检索的字段同步到搜索引擎或分析型数据库分片库只负责基于分片键的在线查询这是最常见的做法。第三是接受聚合对少量低频查询直接并行请求所有分片然后在应用层合并前提是并发数可控、结果集不大。我见过最失败的案例是有人试图在应用层“重新实现join”写了上百行内存关联逻辑维护成本极高最后全删了换成宽表。5.2 分布式事务能不用就不用分库之后本地事务失效这是让很多团队最痛苦的变化。但我要说一句可能不太好听的话绝大多数业务根本不需要分布式事务需要的是“最终一致”。下单扣库存、创建订单、写流水这三件事如果设计成强一致事务代码会很爽但系统会很难受如果改成本地消息表或者事务消息订单先落库再由消息异步扣库存和写流水对账兜底用户体验几乎无差别。确实需要强一致的场景主流方案是TCC或Saga配合Seata这类框架落地。TCC适合短事务、对一致性要求高、资源方愿意提供预留接口的业务Saga适合长事务、可以补偿的业务。我的建议是能用事务消息解决就不上TCC能上TCC就不碰两阶段提交。两阶段提交在分布式数据库场景里性能损耗太大工程落地基本是噩梦。注意分片之后事务边界要重新梳理。很多团队拆完库才发现原来一个本地事务里跨了三个分片最后只能改业务流程。所以初始化分片时一定要把强一致的数据放在同一个分片里分片键的设计天然决定了事务边界。5.3 全局主键的三条路线单库主键自增在分片之后就废了因为多个分片各自生成的自增ID必然冲突。全局唯一ID我做过三种方案各有适用场景。UUID实现最简单、性能也够但36位字符串做索引会让索引体积膨胀随机字符串写入还会导致页分裂查询性能明显下滑只适合对性能和存储不敏感的场景。雪花算法Snowflake64位长整型时间戳机器ID序列号趋势递增、生成速度快是目前最主流的选择。需要注意时钟回拨问题代码里要做时钟偏移判断否则可能生成重复ID。我用过的生产级实现里都会在检测到时钟回拨时等待或拒绝生成。号段模式数据库维护一个发号表每次取一批ID比如1000个缓存在应用内存里用完了再取下一批。优点是ID是严格递增的数字对分页排序友好数据库压力很小缺点是发号表本身是单点高可用要做成多节点带步长。方案是否递增生成性能主要风险UUID否高索引膨胀、随机IO雪花算法趋势递增高时钟回拨号段模式是中发号表单点实际项目我首选雪花算法加上统一的ID生成服务封装业务方无感。5.4 扩容与再平衡预分片是唯一后悔药分片集群最怕的就是扩容。如果是用取模路由且没做预分片扩一个分片意味着几乎全量数据要重新分布过程中还要保证线上可用复杂度极高。这就是我在前面强调预分片的原因一开始就把逻辑分片切到4096个物理分片只有32个扩容时只需把物理分片从32个提到64个迁移其中的逻辑分片即可路由算法和业务代码完全不用动。一致性哈希也可以缓解扩容问题但对我来说预分片方案更好理解、更好排障也更符合多数团队的技术栈。扩容时的数据迁移用和首次迁移一样的手段新分片挂到集群里通过binlog同步接收增量数据再回放存量逻辑分片追平后把路由映射切换过去。整个过程可以灰度也可以随时回退。这个过程务必在业务低峰期做并且每迁移完一个逻辑分片就做一次校验不要等全部迁移完再对账否则出错时定位范围太大。6. 复盘分片之后的隐性成本与止损建议6.1 运维复杂度翻倍而且是乘法不是加法分片之后备份、监控、DDL变更、数据订正所有这些运维动作的复杂度都要乘以分片数。原来一条ALTER TABLE加个索引现在要遍历256个分片逐个执行还要考虑每个分片执行期间的锁和主从延迟。备份策略要从单库全量变成分片并行一致性校验监控面板要从一张变成一整个文件夹。我见过团队在扩容后忘了把新分片接入备份任务直到一次故障才发现某个分片根本没有备份。所以分片集群一定要配套自动化运维平台分片信息注册、DDL变更流水线、备份任务自动覆盖新分片、数据校验定期执行这些在单库时代不是必需品在分片时代都是救命稻草。没有自动化能力就急着上分片等于开着没有仪表盘的飞机。6.2 研发心智负担每一条SQL都要先想路由分片的隐性成本里最容易被低估的是研发心智负担。单库时代写SQL只需要想逻辑对不对分片之后每写一条SQL都得先想分片键带了吗这个查询是单片还是广播跨片排序和分页还能不能做SQL层面的习惯改变是长期的团队新人也需要很长的适应期。我的止损建议有三条一是中间件层把广播查询和跨片查询的日志单独收敛定期review发现一次处理一次二是把分片键约束写进代码规范核心SQL必须走分片键例外查询走独立的索引表或异构存储三是在代码评审里加入路由审查项merge前必须确认没有漏带分片键的查询。这些听着繁琐但能避免线上事故。6.3 线上问题排查的链路被拉长单库时代定位一个问题一条慢SQL日志、一个执行计划就差不多了。分片之后一条请求要经过N个分片问题可能出在路由、某一个分片的负载、或是中间件层的聚合逻辑。排查链路明显变长如果没有全链路追踪和分片维度的监控遇到故障就是大海捞针。我在实践中沉淀下来的最低配置全链路Trace要带上分片号和分片键日志里必须能看出这条SQL最终路由到了哪几个分片数据库监控要按分片维度拆分一个分片CPU飙高不能淹没在集群平均值里慢查询采集要把“分片号表名路由键”作为聚合维度。有了这三样绝大多数的分片问题都能在十分钟内定位。分库分表从来不是银弹它更像一张高利贷信用卡额度很大但利息很高。我现在的第一原则是能用归档解决就不分表能垂直拆就不水平拆能异构存储就不跨片查询。如果已经上了分片的系统也别急着推倒重来把分片键、路由策略、运维自动化这三件事做扎实比换一套中间件或者重新设计架构有用得多。最后提醒一句如果你的系统还没有分片但正在因为数据量而焦虑先去做一次压测和慢查询治理分库分表应该是一张深思熟虑之后才敢动用的底牌而不是第一张打出去的牌。
返回列表