资讯详情

分库分表实战复盘:从分片键到框架选型的完整指南

📅 2026/10/16 11:45:28 | 华诺云谱 👁 阅读
分库分表实战复盘:从分片键到框架选型的完整指南
我刚接手某电商售后系统的数据库优化时订单表已经涨到快到3亿行单库的磁盘占用逼近1TB平时查询只是慢大促期间直接拖垮了主库。那段时间我每天盯着慢查询日志脑子里翻来覆去就四个字分库分表。后来整个项目落地完成订单查询从秒级降到毫秒级扩容也比以前从容得多。这篇内容就是把从方案选型到框架落地的全过程做个复盘聊清楚为什么最终选了这个架构、分片键怎么定、框架接入时哪些坑必须绕着走。不管你是正准备对核心表动手还是已经在分片边缘试探这篇都可以给你一个还算完整的参考。1. 分库分表先想清楚它到底在解决什么问题1.1 单库单表扛不住的不是数据量是请求量很多人一听到“分库分表”第一反应是“表太大了要拆小”。这个说法对了一半但不全对。真正让单库单表撑不住的是单位时间内的并发请求量而不是单纯的存储空间。磁盘是会越用越满但你可以加磁盘、可以归档冷数据纯粹的数据量增长是有办法慢慢处理的。真正要命的是高频写入和高并发查询同时压在一张表上行锁竞争加剧缓冲池命中率下降主从复制延迟变大最终整个集群的响应时间一起恶化。我做这个项目时系统最开始只是慢还没到不可用的程度。但大促压测一上来主库CPU直接冲到90%以上连接数打满后新请求全部排队。这时候你再去调SQL、加索引其实已经是拆东墙补西墙因为问题出在单节点的资源上限上。分库分表的本质就是把原本一个数据库节点要做的事摊到多个节点上并行做。用我最喜欢的一个类比一家餐馆只有一张桌子翻台率再高一天也接待不了多少客人。分库分表就是多开几张桌子、多分几个包间服务员各管各的区域。你不能怪厨师炒菜慢因为瓶颈压根不在灶台上。所以做技术方案之前先别急着喊“拆”。先回答三个问题当前数据量到多少了QPS峰值是多少未来一年预计增幅是多少这三个数字直接决定了你该拆到几张表、分几个库也决定了后续扩容节奏。很多公司分库分表做得痛苦不是因为技术选型错了而是根本没想清楚要解决什么问题就动了手。1.2 垂直拆分和水平拆分两条路线怎么选分库分表通常分为两类垂直拆分和水平拆分。这两条路线不是互斥的实际上大多数成功案例都是先做垂直再做水平。垂直拆分按业务域切分。比如一个电商系统用户、订单、商品、支付、库存这些表原本都在一个库里垂直拆分就是把用户表放到用户库、订单表放到订单库各自独立部署。它的好处是业务隔离清晰不同域的负载不再互相影响坏处是跨库Join会变得很麻烦事务也可能跨库。水平拆分按数据行切分。比如订单表有3亿行按某个分片键通常是用户ID或订单ID取模分成16张表每张表2000万行不到。它的好处是单表数据量明显下降索引更小、写入更快、查询分散坏处是路由逻辑、全局主键、跨节点聚合都成了新问题。在真实项目里你很难说先拆哪个更好更多是看现状。我当时排查的时候发现订单表旁边还挂着订单明细表、退货申请表、发票表都是同一个业务域如果直接按订单ID水平拆这些表也得跟着拆否则Join根本走不了。所以项目第一步不是直接水平切分而是先做应用层的模块拆分把重IO的表独立成服务再考虑分片。这里分享一个比较务实的判断标准如果你的数据表之间强关联少、事务边界清楚垂直拆分收益立竿见影如果一张表的数据增长速度远超其他表横向拆分才是出路。大多数时候你会先垂直后水平把核心大表单独拎出来搞。1.3 分片键整个方案的地基选错直接返工分库分表的所有路由都依赖分片键。分片键选对了查询能精准落到某个分片选错了就要全库扫描比不分片还慢。我经常听到的一个问题“我能不能不指定分片键让框架帮我路由”答案是能但代价很大。比如你不带分片键查询代理层会把请求广播到所有分片再合并结果。你要的数据可能只有10条但底层跑了16个分片的查询IO和CPU反而暴涨。所以分片键的选择一定要围绕你最核心的高频查询维度。以订单系统为例用户查询自己的订单是最常见的场景所以绝大多数系统用buyer_id或user_id作为分片键。运营后台按订单号查那是低频场景可以单独建一个订单号和用户ID的映射索引或者用搜索引擎解决而不是让整个分片体系去迁就后台查询。分片键一旦上线后续改动成本极其巨大。表里的数据已经按旧键散出去了要想换键基本等于重刷全量数据。这点请务必在方案评审阶段反复推敲多问几个“哪个查询才是最高频的”。如果连这个都拍板不了我劝你先别拆。2. 架构演进路线从单库到分布式数据库的必由之路2.1 垂直拆分优先水平拆分兜底在我接触的大量案例里团队对分库分表的预期常常过高恨不得一步到位拆成微服务、上分布式事务、上全局ID生成器。实际上越复杂的架构运维成本和技术债越重。我给当时的团队定的路线很简单先垂直拆再水平拆能不分就不用分。垂直拆分在实施层面相对温和它不改变单表的数据分布只是把不同表挪到不同库。这一步能解决“互相拖累”的问题。举个小例子当时系统里有张配置表数据量只有几千行但被高频读取每次查询都快。反而是订单表几条慢SQL把整个库的IO拖垮配置表也跟着遭殃。把配置表和订单表放到不同库之后配置查询的稳定性立刻上来了。水平拆分则要沉重得多。它不是把表搬家而是修改数据分布方式涉及数据迁移、分布式ID、跨节点事务一个环节出问题就是事故。所以我更倾向把它当“兜底方案”而不是“首选方案”。只在大表本身成为瓶颈、不管怎么垂直拆都解决不了的时候才启动水平拆分。这里也给一个我比较认可的分层方案数据量千万级以下单库单表做好索引和SQL优化即可。数据量千万到亿级垂直拆分 读写分离 缓存通常能撑住比较长的时间。数据量亿级以上且核心路径上持续高频写入水平拆分。大部分业务其实在第一层和第二层就能解决。直接上水平分片经常是“用复杂度换性能”而复杂度的代价往往被低估。2.2 分表算法的工程实践取模、范围、一致性哈希怎么取舍水平拆分选定之后第一个技术决策就是分片算法。常用的有三种取模、范围、一致性哈希。没有绝对的好坏关键看业务匹配度。取模分片是最经典的做法比如shard_id order_id % 16把订单号散列到16张表。它的优点是数据分布均匀实现简单绝大多数框架原生支持。缺点是扩容时要动模数比如从16扩到32几乎所有数据都要重新分布停机迁移是难免的。我在项目里用的是user_id % 16先把用户维度对齐后续如果要做用户维度的查询分析成本会低很多。取模时要注意一个细节如果分片键本身是连续自增的取模结果可能在部分分片特别集中就没法真正打散。所以分片键一定要选一个有足够随机性的东西用户ID和订单ID通常没问题但日期字段要慎重。范围分片是按区间切分比如订单表按create_time分月表每个月一张表或者按ID区间分成多段。它的好处是扩容方便加一张新表就行不需要搬旧数据对“冷热数据”天然友好。坏处是数据可能倾斜严重比如大促那几天生成的订单占了全年的80%那几张表又会重新变成热点。一致性哈希我单独说一下。它本质上是对取模的一种改良引入哈希环和虚拟节点让节点增加时只有部分数据需要迁移。看起来很美但分库分表框架对它的支持参差不齐而且因为哈希环上的“虚拟节点”路由规则比取模复杂排查问题时定位更费劲。除非你的系统需要频繁动态扩缩容否则我不太建议一上来就用它。我的个人选择排序是业务有明显的冷热周期用范围分片业务无状态且追求负载均衡用取模团队基础设施能力强且有动态弹性需求再考虑一致性哈希。2.3 扩容分库分表最怕遇到的事扩容是分库分表架构中最危险的环节没有之一。一次扩容操作一旦迁移脚本写错或者回滚方案不完善丢的就是真实用户数据。我见过最典型的扩容方案是“翻倍扩容法”原先是4个分片新架构直接扩成8个。数据迁移时不是把旧数据从头到尾重新分布而是把每个旧分片“拆成”两个新分片。比如旧分片1里的数据一半到新分片1一半到新分片5。因为是2倍扩容所以可以用一个相对简单的规则new_shard old_shard或者new_shard old_shard 4。听起来很顺但实际操作会碰到几个麻烦。第一旧表数据量很大迁移期间线上流量还在不停写不能直接停机导数据所以一般要先用“影子表”把增量双写起来再分批跑存量最后校验、切流量。第二路由规则切换是一瞬间的事如果新规则有问题得能在秒级回滚这就意味着迁移期间要做灰度发布和监控告警。我当时做扩容时踩过一个坑用了某个团队的迁移工具跑完存量数据后发现新分片的自增主键和旧分片冲突因为这表的主键原本是库内自增拆成多分片后每个分片都从1开始编号全局唯一性完全丧失。后来才意识到分库分表场景下根本不能用单库自增主键必须用全局唯一的分布式ID。这个教训在后面会专门展开。为了减少扩容的伤害我强烈建议在做分片设计时就给未来的扩容留好余地。比如初始分16片但预分配两个物理数据库节点每节点8片。后续扩容到32片时前16片的数据位置保持不变新增的16片放在新节点上只有后16片需要迁移工作量直接减半。这种“预留槽位”的思路在早期规划时花不了几分钟但后期能省几天的迁移时间。3. 框架选型与配置落地把ShardingSphere、MyCat拿来做对比3.1 ShardingSphere-JDBC vs MyCat 核心差异对比框架选型这事网上吵得很凶其实核心就是两条路线客户端分片嵌入式和代理层分片中间件。客户端分片的代表是 ShardingSphere-JDBC。它是集成在应用里的一个依赖应用启动时框架会解析你的SQL根据分片规则改写成多个分片上的SQL再合并结果。优点是性能好不额外增加网络跳数连接管理走应用原有的连接池。缺点是对有多个异构语言团队的公司不友好比如你既要Java服务又要Go服务分片规则和依赖就得各做一遍。代理层分片的代表是老牌的 MyCat 以及它的生态。代理是独立部署的一个中间件服务应用连它就像连一个普通的MySQL由它把请求转发到后端真实分片。优点是对应用几乎零侵入任何语言都能接运维也能统一处理。缺点是多了一次网络转发吞吐量有损耗而且代理本身变成了新的单点需要额外高可用设计。我最终选了 ShardingSphere-JDBC。原因也很简单当时的核心交易链路是Java生态团队里没有专门做数据库中间件运维的编制不想多维护一套代理集群。而且 ShardingSphere-JDBC 在同等硬件条件下性能明显优于代理层方案——它不是被转发到代理再转发到分片而是从应用侧直接路由到目标分片。给你一个粗略的对比表我用下来比较有体感的几个维度维度ShardingSphere-JDBCMyCat部署形态应用内嵌依赖独立中间件服务对应用侵入性需要改数据源配置和部分SQL低连接代理即可多语言支持弱主要为Java生态强任何能连MySQL的语言都可以性能损耗低直连分片库偏高多一次网络转发运维成本低随应用发布高需单独维护代理集群SQL支持范围有限复杂SQL需改写有限复杂SQL需改写无论选哪种框架有一点是共同的不要指望框架能完美解析所有SQL你迟早要面对“改写SQL”这件事。与其后期跟框架较劲不如在项目初期就规范团队的SQL写法尤其是别写多表Join、别写子查询当主查询、别用SELECT *。3.2 ShardingSphere-JDBC配置一个最小可运行的订单分片我拿当时订单表的最小配置做一个示例。假设你有两个物理库ds0和ds1每个库一张订单表t_order分片规则是user_id % 2。注意这不是生产级配置但足够把整个路由逻辑跑通。配置大概长这样dataSources: ds0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds0 username: root password: root ds1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds1 username: root password: root rules: - !SHARDING tables: t_order: actualDataNodes: ds${0..1}.t_order tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: order_inline keyGenerateStrategy: column: order_id keyGeneratorName: snowflake shardingAlgorithms: order_inline: type: INLINE props: algorithm-expression: ds${user_id % 2} keyGenerators: snowflake: type: SNOWFLAKE这份配置里几个细节值得展开说下actualDataNodes定义了数据节点的分布方式ds${0..1}.t_order表示在ds0和ds1两个库中都有一张t_order表。shardingColumn指定分片键是user_idalgorithm-expression里的${user_id % 2}是路由表达式。框架解析SQL后会根据传入的user_id参数值直接计算目标分片。keyGenerateStrategy配置了分布式ID生成策略这里用的是雪花算法生成主键order_id时不会依赖数据库自增避免多分片主键冲突。这套配置跑起来后框架会把“插入订单”语句改写成ds0或ds1对应库的插入语句同时自动填充order_id。对业务代码来说你仍然是在“使用一个逻辑上的数据源”大部分底层细节被屏蔽掉了。这也是ShardingSphere-JDBC最舒服的地方改造时不用动业务代码核心逻辑主要是改数据源和写分片规则。但也要清醒一点这份配置隐藏了很多“边界”。比如查询没有带user_id框架就会广播到两个库执行比如多表关联时如果没配置绑定表或广播表Join可能直接在错误分片执行或者被拒绝。这些问题会在后面章节专门讲。3.3 分布式ID、读写分离、广播表这些周边必须一起搞定分库分表不能只盯着分片本身周边的几个能力没跟上系统是无法稳定运行的。先讲分布式ID。分库分表之后单库自增主键彻底不可用最简单的替代方案是雪花算法。雪花算法生成的ID是一个64位整数由时间戳、机器ID、毫秒内序列号组成在分布式环境下基本能保证趋势递增和全局唯一。它的好处是不需要额外的服务应用本地就能生成性能极高适合高并发写入。要注意的是机器ID的分配管理如果运维不规范多实例部署时可能会撞ID这问题排查起来非常隐蔽。再讲读写分离。分库分表后主库的只读查询也可以继续分流到从库但前提是分片和读写分离的规则要能同时生效。ShardingSphere 的!READWRITE_SPLITTING规则可以跟分片规则叠加。我当时的做法是主库负责写入和强一致读从库负责报表类、运营类的非实时查询。这样主库的压力就进一步降低了。广播表也要重点说。某些表的数据量很小但几乎所有分片查询都要用到它比如商品分类表、订单状态字典表。这种表如果只放在某个分片上跨分片Join基本无解。所以框架提出了“广播表”的概念把这类小表在每个分片库里都复制一份查询时直接本地关联。代价是写入时要同步到所有分片但这换来了查询端极大的简化很值得。提供一些相关提示绑定表和广播表不一样绑定表是指有相同分片键的两张表比如订单表和订单明细表都用user_id分片这样Join可以在同一个分片内完成避免跨库Join。配置绑定表非常关键如果没配框架就会对两边都做全分片扫描再归并性能直接崩掉。4. 分库分表之后的那些坑事务、查询、迁移一个都躲不掉4.1 跨节点事务柔性事务是正则解分库分表之后本地事务的边界被打破了。一个业务操作可能同时修改ds0的订单表和ds1的库存表传统数据库的BEGIN...COMMIT已经管不到两个库。这时候你面临两个选项分布式硬事务和柔性事务。分布式硬事务一般用两阶段提交XA协议实现能保证强一致性但性能代价很大协调者本身也会成为瓶颈。在高并发交易链路里我用过的多数团队其实都放弃了XA原因很简单太慢了而且一旦协调者挂了整个事务卡死恢复流程非常麻烦。柔性事务是更务实的选择。核心思想是“最终一致性”不要求每个时刻数据都一致但通过补偿、重试、对账等手段保证在一段时间后数据最终一致。最常用的模式是本地消息表 消息队列。比如创建订单时先在本库写入订单和一张本地消息表事务提交后通过消息队列异步通知其他系统更新库存。如果中间环节失败就不断重试本地消息表中的记录直到对方确认成功。我在项目里遇到过一个更恶心的场景订单创建后要扣减优惠券优惠券系统在另一个库如果用柔性事务确实能保证最终一致但用户在下单后的几秒内可能看到优惠券“仍然可用”如果用户连续点击下单就可能超发。最终我们引入了一个前置预占逻辑下单前先在优惠券系统预占额度预占成功才允许创建订单超时自动释放。这算是用业务规则来规避分布式一致性问题比纯技术方案更稳。关于这一点我想强调分布式事务方案没有银弹。你越依赖框架去解决一致性问题越容易踩坑。最好的方式是在业务流程设计上尽量避免跨节点事务比如把强一致的写操作限制在同一个分片内跨分片的部分尽量做成异步化。4.2 跨库Join、分页排序换个思路来处理分库分表之后原先一张SQL能完成的Join、分页、排序现在都会变成多分片的数据合并。很多时候最简单的方案不是让中间件去改写SQL而是从业务查询方式上绕过去。跨库Join最直接的解法是“数据冗余”。比如订单列表页要显示商品名称你不需要每次Join商品表而是在订单表中冗余一个product_name字段。写入订单时一次性把名称带过来查询时直接读。数据冗余会增加写入负担和存储成本但查询性能提升非常明显。另一个思路是“多处存储各取所长”。我的团队后来引入了一个开源的搜索引擎订单创建后把搜索所需的字段同步一份到ES后台运营的复杂查询、多条件筛选、分页排序全走ES数据库只负责按主键或分片键查询。这个方案的改造成本不低但换来的是查询模型的彻底解放。分页排序这个问题更隐蔽。假设你要查第100页的第10条数据单库时直接LIMIT 990, 10就行但分库分表后每张分片都要先取前1000条再由中间件归并排序最后截取第990到1000条。这意味着你翻了多深的页中间件就要在每张分片上捞多深的数据性能呈线性下降。深分页基本是分片架构下的明显痛点。我的经验是业务侧尽量避免深分页。如果一定要做可以改成“游标式翻页”比如基于上次返回的最后一条ID继续向后查只做WHERE order_id ?的查询不依赖页码偏移。搜索引擎场景就更不必说ES本身对深分页也有自己的方案同样建议用游标。这个思路从根本上避免了“查两页数据、捞半张表”的尴尬。4.3 数据迁移双写方案与校验回滚不管你是从单库拆到分片还是从4分片扩到8分片数据迁移都是一场大考。最稳的做法是“双写 校验 灰度切读 回滚预案”。双写的核心思想是在迁移期间业务写入同时写到旧表和影子表新表旧表继续承担线上流量影子表累积增量数据同时存量数据用分批任务导入影子表。等到影子表的数据追平旧表经过比对校验后把读流量逐步切到新表最后再把写流量切过来。这里要特别注意“时间窗”的处理。存量数据导入期间线上还在不停产生新数据如果只靠导入任务抓取很容易漏掉那些边导入边写入的数据。所以双写必须从项目启动的第一天就开启而不是等存量导入快结束才开始。增量数据保持一致后再用一个校验工具按主键或唯一键逐条比对常见的做法是查一条新库数据再回查旧库对应数据比较字段是否一致。回滚预案必须在迁移前设计好。我的经验是切流量必须做成“开关式”而不是直接改代码重启。可以用配置中心动态控制流量切到新库的比例先放10%观察监控没问题再放到50%逐步升到100%。一旦发现数据不一致或者报错率升高秒级把流量切回旧库。这比任何“事后回滚”都有效因为你在灰度阶段就把风险消化掉了。当时我负责的迁移项目执行了整整一个周末。周五晚上启动双写周六跑存量导入和校验周日白天灰度切10%流量确认无误后深夜切全量。整个过程最耗时间的不是写代码而是等待校验结果和观察监控。如果你没有完整的监控大盘我建议先别动迁移否则出了问题你连“哪里不对”都看不出来。5. 常见问题速查表与避坑心得5.1 高频问题与排查方向分库分表落地之后日常运维中会稳定复现一些典型问题。我整理了一份速查表基本覆盖了大部分团队会遇到的状况问题现象可能原因排查与解决方向查询不带分片键全库扫描接口响应变慢分片键未作为查询条件检查业务SQL是否带分片键不能带就考虑加映射表或搜索引擎已带分片键但查询仍广播所有分片分片算法表达式写错或参数类型不匹配确认SQL参数类型与分片键字段类型一致查看框架路由日志主键冲突或ID重复分库后仍用单库自增主键切换为分布式ID生成方案比如雪花算法插入数据时提示“找不到分片”分片键为NULL或未传入检查实体字段映射确认写操作都设置了分片键多表Join返回数据错误未配置绑定表或广播表将同分片键的表配置为绑定表小表配置为广播表分页翻页越深越慢深分页导致所有分片聚集大量数据改为游标式翻页或使用搜索引擎大促期间某几个分片负载特别高分片键分布不均产生数据倾斜重新评估分片键的散列度必要时更换分片键金额类数据偶尔对不上账跨分片事务存在中间状态引入对账任务加柔性事务补偿逻辑这张表不是让你出了问题再查的而是建议你在上线前对着过一遍把潜在坑提前填掉。比如“查询不带分片键”这件事完全可以靠网关参数校验、代码评审规范来规避。5.2 几条我踩过坑后的独家经验关于分库分表的架构落地我最后分享几条自己的心得。这里有一些是从教训里提炼的还有一些是规划时就应该注意的能帮助你少走弯路。第一不做分库分表才是最好的方案。这话听起来像废话但真的很多团队是被“技术潮流”推着走的。你的数据量、QPS是否真的到了单库的极限尝试过索引优化、缓存优化、归档冷数据吗如果都没试过直接上分库分表那就是用最大的架构复杂度换一个可能不需要解决的问题。第二分片键的讨论永远不要嫌多。业务之后一定会提出各种新查询如果分片键选得不贴合核心查询后期每加一个查询可能都要走全分片广播。上线前多花一天时间确认分片键顶得上线后一个月填坑。特别是电商类系统如果管理员后台频繁按订单号查不到、非得用会员ID查那你的映射机制最好提前设计好。第三监控比框架更重要。框架只负责路由和合并但整个分片集群的健康度你得靠监控系统才能掌握。分片间的负载差、慢查询分布、主从延迟、全库广播次数这些指标都要在切换流量之前能一眼看到。没有可靠的监控我不建议任何人草率启动分库分表改造。第四迁移期间一切以“可回滚”为第一原则。灰度切流、开关控制、数据校验都是为了保证你随时能退回去。千万不要抱着“这次必须成功”的心态做迁移越没有退路越容易出大事故。第五留好“槽位”给未来的扩容一点余地。在分片数量设计上我建议预留多余物理节点。比如现在只需要4个库你可以先按8个库的规模规划剩余节点空闲着。这样未来扩容时数据迁移量更小风险也能大幅降低。这个决策在早期可能看着有点浪费但在关键时刻会省下很多麻烦。用我自己的感受来总结分库分表这门功课其实是在“业务增长压力”和“系统复杂度”之间做平衡。同一个方案用一个复杂的中间件砸下去可能也能跑但如果能通过合理规划分片键、控制SQL写法和约定查询规范来降低整体复杂度才是长久之计。如果你正准备动手建议先把上面这些内容消化一遍再结合自己的业务梳理一张分片键候选清单找团队里懂业务的同学一起评审。分库分表不可怕可怕的是想得很简单就冲上去改。祝你的系统能顺利扛住下一波增长。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑