news 2026/10/9 3:39:49

MyCat水平拆分实战:分表规则选型、路由原理与副作用全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyCat水平拆分实战:分表规则选型、路由原理与副作用全解析

上周有个朋友在技术群里发来一张监控截图,单表数据量已经三千多万,几个常用的查询从原来的几十毫秒涨到了快两秒,问我说下一步到底该怎么办。这种局面我见得太多了——MySQL单表数据量跨过千万之后,就算你天天优化索引、调整buffer pool,响应时间终究会走下坡路,最后不得不面对分库分表这道坎。这篇文章是MyCat系列的第5章,专门讲水平拆分中的分表,我会把判断拆分时机、核心路由原理、分片规则选型、分表引入后的副作用以及实测数据一次讲清楚,适合正在做分库分表方案设计,或者已经在MyCat里配置过分表但还想把原理吃透的同学。读写分离只是在用从库分担读压力,水平拆分则完全换了一种思路:把一张大表拆成多张小表,分散到多个数据节点上,每个节点只持有完整数据的一个子集,本质是用机器数量换单机压力。

1. 先判断是不是真该拆:单表撑不住的三种信号

1.1 数据量过千万后,单表究竟在承受什么

很多人对"多少数据量才需要分表"没概念,我习惯用一棵B+树的视角来理解这个问题。MySQL的InnoDB索引就是B+树,三层索引树配合合理的主键设计,大概能支撑两千万行以内的等值查询。等数据量继续上涨,索引树就可能从三层长到四层,每一次索引查找都多一次磁盘IO,而磁盘IO的延迟是内存IO的几万倍,这是单表性能恶化的第一个物理边界。

另一个容易被忽略的是写放大。每插入一行数据,主键索引和每一个二级索引都要更新,如果表上还有频繁的update和delete,页分裂、undo log会跟着一起膨胀。当InnoDB的buffer pool装不下热数据,磁盘读的比例就会上升,慢查询变多只是表象,伴随而来的往往是CPU的sys占用升高、磁盘iowait飙高,整个实例像被掐住了喉咙。

所以"单表能不能撑住"不是看现在有多少行,而是看增长速度以及查询模式。很多团队等到两千多万行才感觉到慢,其实数据量在千万级别就已经开始有端倪了,只是被缓存和不太苛刻的业务容忍度掩盖了。真等所有查询都慢下来再动手,迁移成本会比现在翻好几倍。

1.2 三种典型信号,以及什么样的业务不适合拆

我一般建议团队出现以下三种信号中的至少两条,就开始做分表规划和评估:

  1. 慢查询日志里,单表大范围扫描的查询占比持续升高,而且索引优化已经做过一轮,收益越来越小。
  2. 主库写入并发触顶,磁盘IO、复制延迟经常报警,读写分离已经救不了写压力。
  3. 按当前增长速率估算,单实例的磁盘或内存容量撑不过一年。

当然,并不是所有表都适合水平拆分。适合拆的表,一定要有明确的分片键,比如user_id、order_id这种业务主键,而且大多数查询都能带上这个键。各个分片的数据之间尽量独立,不要频繁跨分片做join。反过来,如果你的查询经常是全表扫描、报表聚合,或者根本没有一个稳定且分布均匀的字段可以做分片键,那我劝你先别急着拆,这种情况下拆完只是把单表慢查询变成了多个分片的慢查询合并,性能可能更差。

另外提醒一句:分表不是银弹,MySQL单机性能还撑得住的时候,不要为了架构上的"先进"而拆。每引入一个中间件,都意味着多一层故障点和运维成本,我的原则是"能拆得清清楚楚才拆,拆完要能说清楚数据到底在哪"。

2. MyCat水平拆分的工作机制:一条SQL如何找到它的分片

2.1 逻辑表、数据节点、分片键三个概念必须先分清

做MyCat分表,首先要把三个概念刻在脑子里:逻辑表、数据节点、分片键。

逻辑表,是你在MyCat逻辑库里面看到的表。这张表在物理上并不真实存在完整的一份,它只是用户视角的映射。数据节点用dataNode表示,每个数据节点指向一个真实的MySQL物理库,是数据实际存放的地方。分片键,则是MyCat路由时的判断依据,它决定了一条SQL该发往哪个数据节点。

举个例子。你在逻辑库ORDER_DB里建了一张order表,这张order表在schema.xml里配置了三个数据节点dn_order_1、dn_order_2、dn_order_3,指定分片键是order_id。那么对应用户来说,order是一张表;对数据库来说,order的数据被切成了三份,分别放在三个不同的MySQL实例里。你执行SQL时不会去关心数据在哪,MyCat会替你做这个路由决定。

分片键的选择是整个分表方案里最重要的一环。它必须满足"业务上高频查询能带上、数据分布足够均匀、值基本不变"这三个条件。选错了分片键,后面所有分片规则设计都等于白做。

2.2 schema.xml和rule.xml的配合方式

MyCat的逻辑配置集中在两个文件:schema.xml负责定义逻辑库、逻辑表、数据节点和数据主机,rule.xml负责定义分片规则和分片算法。一张表要被水平拆分,需要同时在这两个文件里配置到位。

先看schema.xml的最小配置:

<mycat:schema xmlns:mycat="http://io.mycat/"> <schema name="ORDER_DB" checkSQLschema="true" sqlMaxLimit="100"> <table name="order" dataNode="dn_order_1,dn_order_2,dn_order_3" rule="order-mod" /> </schema> <dataNode name="dn_order_1" dataHost="dh1" database="db_order" /> <dataNode name="dn_order_2" dataHost="dh2" database="db_order" /> <dataNode name="dn_order_3" dataHost="dh3" database="db_order" /> <dataHost name="dh1" maxCon="200" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native"> <heartbeat>select user()</heartbeat> <writeHost host="hostM1" url="192.168.1.11:3306" user="mycat" password="mycat" /> </dataHost> </mycat:schema>

table标签里的dataNode指定这张逻辑表分布在哪些数据节点上,rule指定分片规则名称。order表被分到三个数据节点,每个数据节点对应一台MySQL主机上的db_order库。注意这里我用了三个不同的dataHost,表示三台MySQL实例;如果只是单机多库,可以把三个dataNode的dataHost都指向同一个物理机。

再看rule.xml里对应的order-mod规则:

<tableRule name="order-mod"> <rule> <columns>order_id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule> <function name="mod-long" class="io.mycat.route.function.PartitionByMod"> <property name="count">3</property> </function>

columns指定分片键是order_id,algorithm指向函数mod-long,函数类是PartitionByMod,count设为3,意思就是按order_id对3取模。

schema.xml里的checkSQLSchema="true"建议开启,它能把客户端SQL里的库名前缀自动去掉,避免路由时因为schema不一致出问题。sqlMaxLimit="100"是兜底保护,当客户端SQL没有带limit时,MyCat自动补一个limit 100,防止不带条件的大查询把所有分片的数据全拉回来。

2.3 一次写入和一次查询的路由过程

配置完成后,一条SQL的流转路径非常清晰。比如执行insert into order(order_id, buyer_id, amount) values(7, 1001, 99.00),MyCat会解析这条SQL,提取出分片键order_id的值7,交给mod-long算法计算 7 % 3 = 1,于是路由到第二个数据节点dn_order_2,真正落在dn_order_2对应的MySQL库上。

查询是同样的逻辑。select * from order where order_id = 7,MyCat计算出7对应的分片,只把SQL发到dn_order_2执行。这个场景对性能是友好的,因为一个简单查询只打一个节点,不产生跨节点合并。

但如果查询条件里没有分片键,比如select * from order where buyer_id = 1001,MyCat没法判断该去哪,只能把这条SQL广播到三个数据节点上执行,然后在中间件层合并结果。单条查询变成三份查询,再汇总一次,响应时间自然比单表直接查要慢。这也是分表后必须强调"查询尽量带分片键"的原因。

3. 分片规则怎么选:取模、枚举、范围与哈希的取舍

3.1 partition-by-mod取模分片:最稳妥也最需要提前规划扩容

取模分片是MyCat最容易上手的一种规则,上面的order-mod就是典型。它的优点是数据分布足够均匀,实现简单,适合分片键是整型、没有明显业务维度、数据量相对平均的场景。

但取模分片有一个必须提前想清楚的硬伤:扩容。假设当前是三个分片,order_id=5的数据按 5 % 3 = 2 落在dn_order_2。当业务增长需要扩容到四个分片时,同样的 order_id=5 按 5 % 4 = 1 应该落在dn_order_1。这一规则变化会让几乎所有历史数据的分布都发生改变,需要做一次大规模的数据迁移,而不是简单加一台机器就完事。

所以用取模分片前,一定要估算未来两三年内的分片数量,尽量把分片数定得宽裕一点。比如预估未来会到三台,干脆直接上四台或五台,避免短期内扩容。真有扩容需求,也要提前准备好数据迁移脚本,用mysqldump导出、再按新规则导入,或者借助MyCat周边工具做在线迁移,同时做好业务停写窗口的预案。

3.2 partition-by-enum和partition-by-range:按业务固有维度划分

枚举分片和范围分片都是"顺着业务维度"来划分数据的办法。

枚举分片适合分片键是有限离散值的场景,比如按省份、城市、业务线来拆。配置上使用PartitionByFileMap,通过mapFile指定一个映射文件。比如按省份拆的partition-map.txt里写入:

10001=0 10002=1 10003=2

第一列是分片键的值,第二列是数据节点下标。type=1表示Key为整型,如果Key是字符串则把type设为0。枚举分片的优点是路由直接、查询可以精确落到对应分片,缺点是如果枚举值分布不平均,比如某个省份的数据量特别大,热点问题会非常突出,需要单独给热点省份再细化规则。

范围分片则用AutoPartitionByLong,在autopartition-long.txt里写区间:

0-1000=0 1001-2000=1 2001-3000=2

这种规则适合分片键单调递增、且你能大概预测区间分布的场景,比如按用户ID范围、按订单序号范围分片。它的好处是支持范围查询裁剪,查询条件落在哪个区间就去哪个分片执行;缺点是数据写入往往集中在最新区间,导致最后一个分片成为热节点,前面几个分片相对空闲。用范围分片前,最好确认一下业务是否存在明显的冷热分布,并且愿意接受热节点运维上的倾斜。

3.3 partition-by-murmur哈希:扩容友好的一条路

如果你既想要均匀分布,又希望未来扩容时不用全量迁移,可以看看PartitionByMurmurHash。这个算法基于MurmurHash,把分片键哈希到虚拟桶上,再映射到物理分片:

<function name="murmur" class="io.mycat.route.function.PartitionByMurmurHash"> <property name="seed">0</property> <property name="virtualBucketTimes">160</property> <property name="bucketMapPath">/opt/mycat/conf/bucketMap.log</property> </function>

virtualBucketTimes是一个分片键被虚拟映射的次数,值越大分布越均匀,但内存占用也会增加。它的一大优势是扩容时只需要迁移部分数据,不用像取模那样把所有数据都翻一遍。不过分片数量比较少的时候,MurmurHash的均匀程度未必比取模更漂亮,而且SQL的定位过程会比取模稍复杂一点点,排查问题时不如取模直观。

三种常见规则我做了个对比,方便你选型的时候快速拍板:

规则配置类扩容友好度分布均匀性适合场景
取模PartitionByMod差,需要大规模迁移好整型分片键、无业务维度、数据总量大
枚举PartitionByFileMap中等,按枚举增量调整取决于枚举分布按省份、业务线、等级等离散维度
范围AutoPartitionByLong中等,按区间追加可能热点倾斜按时间、序号、ID区间分片
一致性哈希PartitionByMurmurHash好,只迁移部分数据较好未来频繁扩容、数据量大且分布要求高

4. 分表之后绕不开的四大副作用:全局表、ER关系、主键与深分页

4.1 全局表:让小字典表在每个分片都有一份完整副本

分表之后,第一个不舒服的地方是join。比如order表按order_id分片,order_status_dict这种字典表如果只有一份,join的时候MyCat就需要跨分片拉数据,代价非常大。MyCat给出的标准方案是全局表,在schema.xml里配置type="global":

<table name="order_status_dict" dataNode="dn_order_1,dn_order_2,dn_order_3" type="global" />

全局表的含义是:每一个数据节点都保存一份完整的字典表副本。查询时MyCat随机挑一个分片返回数据,更新时则把写操作广播到所有分片,保证每个副本一致。

适合做成全局表的通常是状态字典、配置类数据、维度类小表,数据量控制在几千行以内为佳。如果全局表太大,每个分片都存一份完整副本会让存储成本成倍上涨,同时高并发写场景下广播更新的开销也会被放大。我见过有人把几百万行的历史维度表也配成全局表,结果每个分片多出几百万冗余数据,这个思路不可取。

4.2 ER分片表:把join留在同一个分片内

全局表能解决小字典表的join,但业务里更常见的是"主表+明细表"的结构,比如order和order_item。主表拆分后,如果我们希望这两张表的join依然能下推到单个MySQL节点,就要用ER分片。

配置方式是在schema.xml的order表下挂childTable:

<table name="order" dataNode="dn_order_1,dn_order_2,dn_order_3" rule="order-mod"> <childTable name="order_item" primaryKey="id" joinKey="order_id" parentKey="order_id" /> </table>

childTable里的joinKey是子表的分片键,parentKey是父表的分片键。MyCat在插入order_item时,会根据order_id的值计算出它的父记录所在分片,把明细数据写到同一个分片里。这样order和order_item的join就被限制在单节点内执行,MyCat可以直接下推SQL,不需要跨分片合并。

使用ER分片的关键前提是:join关联字段必须就是分片键。如果两张表的join字段不是分片键,ER配置帮不了你。这时候优先考虑冗余字段方案,比如在order_item里冗余一个buyer_id,让子表按buyer_id分片,查询时直接带buyer_id。

4.3 分布式全局主键:自增ID下岗之后谁来发号

单表时代的自增ID,在分表之后完全不可用。原因很简单:三个分片各自维护自增计数,极大概率会生成重复ID,主键冲突会把写入直接打挂。即使你通过autoIncrement配置让每台机器设置不同的起始值,也只是临时的缓兵之计,无法支撑动态扩缩容。

MyCat提供了自己的全局序列方案。最简单的本地文件方式适合并发不高的场景,生产我建议用数据库方式,建一张MYCAT_SEQUENCE表来维护序列:

CREATE TABLE MYCAT_SEQUENCE ( name VARCHAR(50) NOT NULL, current_value INT NOT NULL, increment INT NOT NULL DEFAULT 100, PRIMARY KEY(name) );

配合MyCat官方提供的三个存储过程mycat_seq_currval、mycat_seq_nextval、mycat_seq_setval来取序列值。每次取号不是取1,而是按increment批量取一段,MyCat在内存里发号,用完了再到数据库要下一段,从而减少对数据库的频繁访问。这块配置细节比较多,生产环境也要额外保证MYCAT_SEQUENCE表所在库的高可用。

如果不想引入额外的序列配置,业务上也可以用雪花算法、Redis的incr或者中心化发号服务。我个人的偏好是:能交给统一发号器就交给统一发号器,因为分布式环境下全局唯一且趋势递增的ID,对MySQL的B+树索引是最友好的。

4.4 深分页和跨分片聚合:limit没你想的那么简单

分表之后,分页查询是重灾区。单表上limit 1000000, 20直接走索引扫描还行,在MyCat分片模式下,这个SQL会被改写成分片节点各自执行limit 0, 1000020,然后把三个节点返回的300多万行数据在MyCat内存里排序、去重,再丢掉前一百万行,最后取20条返回。这中间的排序和网络传输开销非常惊人,所以深分页会让MyCat节点内存暴涨,甚至OOM。

解决深分页最有效的办法是改成"键集分页"——通过上一页最后一条记录的ID往下翻:

select * from order where order_id > 1000000 and order_id <= 1000100 order by order_id limit 100;

用order_id这种分片键有序字段做游标,每次查询都能精确路由到单个或少量分片,响应时间几乎恒定。如果业务上非要支持任意的深层页码跳转,就要在设计阶段砍掉翻页深度,一般产品上用户翻到几十页之后就不再继续翻了,这是可以在需求层面沟通的。

跨分片的order by、group by、count、sum操作也是类似道理,MyCat会把SQL下发到每个分片执行,再在中间件层汇总。数据量一大,这些聚合操作都要求MyCat有足够大的JVM堆内存,建议部署时把wrapper.conf里的-Xmx调大,并且预留独立的机器资源给MyCat,别和业务应用抢内存。

5. 三节点分片实测:拆与不拆的差距到底有多大

5.1 测试环境与表结构

光讲原理还不过瘾,我把之前一次压测的实测数据整理了出来。测试环境不是生产,但基本能反映分片前后的量级差异:

项目配置
MySQL版本5.7,三台2C4G云主机
MyCat版本1.6.7.6
表结构t_user(id bigint, user_name varchar(50), city_id int, created_at datetime)
单表模式单表3000万行,独立实例
分片模式三节点各1000万行,按id取模分片

5.2 性能结果对比

场景单表3000万MyCat三节点各1000万结论
等值查询命中索引约8ms约6ms提升不明显,因为本来就是索引查找
范围查询返回100条约45ms约28ms分片并行有优势
count(*)整表约22s约9s分片收益明显,三节点并行扫描
limit 1000000, 20约3.5s约2.1s分片看似占优,但MyCat内存合并开销大,深翻页仍危险

等值查询的提升之所以不大,是因为单表在索引命中的情况下本来就很快,分片并不会把磁盘随机读变成内存读。分片真正发挥价值的地方在于大范围扫描和聚合类操作,三节点并行确实能把总耗时压下去。但注意,如果查询没有带分片键,MyCat要广播到三个节点再合并,很多场景反而比单表更慢,这一点在高并发下会被成倍放大。

5.3 几条踩过之后不想你再踩的坑

按重要程度排一排:

第一,分片键字段绝对禁止update。一旦分片键的值发生变化,记录实际所在节点和规则计算出的目标节点就对不上了,这条数据会变成"逻辑失踪",查也查不到,删也删不掉。我在线上见过一次因为业务逻辑改了user_id导致数据错乱的案例,恢复起来极其痛苦。

第二,不带分片键的update和delete是高危操作。MyCat会把它们广播到所有分片执行,一旦你的where条件写得不精确,就是全量数据被误改。这类SQL应该被列入发布评审的红线,代码review时见一次打一次。

第三,分布式事务不要指望MyCat替你扛强一致。MyCat提供的分布式事务能力有性能损耗,真正对资金、库存这类强一致场景,我建议还是用业务层面的最终一致方案,比如本地消息表加消息队列、TCC等,中间件只做分片路由,别让它顺带帮你解决所有分布式问题。

第四,MyCat本身也是要运维的。它的日志量不小,JVM参数、连接数、前后端连接池都需要监控,不要把MyCat当成一个无状态代理随手一扔。建议部署两台做高可用,毕竟它挂掉,整个逻辑库就不可用了。

第五,注意所有分片节点的MySQL配置要一致。字符集、sql_mode、时区这类参数如果不统一,同一个SQL在不同分片上的执行结果可能不一样,排查起来非常隐蔽。上线前最好做一次分片间的数据一致性校验,哪怕只是抽样对比几条记录、几个count。

如果你问我到底什么量级才值得上MyCat做水平拆分,我的回答是:先看分片键能不能选好,再看团队有没有人愿意持续维护这套架构。分表不是一步到位的终点,它是你数据架构演进的中间过程。把路由规则、全局主键、ER关系这些基础打牢,后续流量再涨,你至少能从容地加分片、迁移数据,而不是在大半夜被线上报警逼着重启一台快被压垮的MySQL。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:39:47

递归与DFS深度解析:核心模板、状态管理及常见题型

这个系列写到第25期&#xff0c;递归却是我一直没敢轻易动的题目。原因很简单&#xff1a;递归这东西看示例都觉得挺好懂&#xff0c;自己一到代码面前就容易卡壳&#xff1b;而DFS——深度优先搜索——又是递归里最典型的那个应用。带过一些刚开始接触算法的人之后&#xff0c…

作者头像 李华
网站建设 2026/10/9 3:39:20

本福特定律:数据审计中的首位数字密码与实战应用

晚上在整理一周采集到的数据&#xff0c;越看越觉得不对劲。明明是从不同渠道收集的“普通数据”&#xff0c;数字开头的分布却一点都不普通&#xff1a;以1开头的记录占到了三成左右&#xff0c;以9开头的记录连5%都不到。第一直觉告诉我&#xff0c;这可能是程序有bug&#x…

作者头像 李华
网站建设 2026/10/9 3:38:41

DeepSeek-VL微调CT报告生成:可解释医疗AI落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 3:38:40

Spring Boot与Spark构建共享单车数据存储与聚合系统

简介&#xff1a;针对SpringBoot与Spark结合开发共享单车数据存储系统的毕业设计项目&#xff0c;完整包含后端Java源码、前端Vue页面、论文文档及数据库脚本。项目以共享单车使用数据为场景&#xff0c;演示了从数据采集、分布式存储到Spark分析处理、SpringBoot接口交付的完整…

作者头像 李华
网站建设 2026/10/9 3:38:34

基于Spring Boot和深度学习的蘑菇识别系统全栈开发实践

每年毕业季最让人头疼的不是论文查重&#xff0c;而是“题目到底选什么”。如果你刷到这篇内容&#xff0c;多半已经在“管理系统、商城、图书借阅”这类老面孔里看花了眼。今天聊的这个题目值得重点考虑&#xff1a;基于 Spring Boot 深度学习的蘑菇种类识别系统。它不是一个…

作者头像 李华
网站建设 2026/10/9 3:37:39

登录态复用与token机制详解:从双token到SSO无感刷新

每次打开后台系统都要重新输一遍账号密码&#xff0c;切到另一个系统又得再来一次&#xff0c;找密码、收验证码、等短信&#xff0c;一天下来光登录就耗掉好几分钟。更难受的是&#xff0c;明明刚登录过&#xff0c;点个链接跳转另一个子系统&#xff0c;又让重新登录。这种体…

作者头像 李华