简介:这是一份面向MySQL中间件使用者的按月分表增强包,基于MyCat 1.6.7.6正式版源码二次开发。核心改进是新增subTables的BYMONTH分表方式,支持“tableName_$202101-?”这类正则配置,从设定月份开始,问号自动动态代表当前月份,无需手动维护完整表名;子表需先在MySQL中真实存在,并可借助动态建表实现子表按月自动增长,大幅降低按月分区的维护成本,对需要保留历史流水、按月归档的业务场景尤其适用。压缩包共111个文件,包括53个jar依赖、18个properties配置、14个txt说明文档、10个xml规则文件、4个sh运维脚本、2个sql初始化脚本等,整体约24.86MB;其中properties和xml用于调整分表与服务规则,jar为运行时依赖,txt和sh提供改造说明与启停辅助,资料组织完整。当前已有375人学习下载。通过该包可快速获得按月分表的可运行实现,参考其中的源码改动与正则匹配思路,还能进一步拓展到季度、年度等其他时间维度分表场景,适合MyCat使用者与源码二次开发人员参考。
1. mycat-1.6.7.6_BYMONTH.zip:把单表千万数据按月拆开,业务 SQL 几乎不用改
当一张订单表以每天几十万行的速度增长,单表行数冲上千万之后,即使索引全部命中,磁盘 IO 和行锁竞争也会让延迟肉眼可见地上升。删数据不舍得,加机器没预算,业务 SQL 又改不动,这是大多数团队第一次意识到需要分库分表的时刻。mycat-1.6.7.6_BYMONTH.zip 就是这类场景下可以直接投入的方案:它是一个 MySQL 数据库中间件发行包,自带按月分片的 PartitionByMonth 算法,应用连接 Mycat 就像连接一个普通 MySQL,业务 SQL 基本不用动,就能把一张逻辑大表按月份拆到多个物理库表。
这个方案适合三类人:单表数据量已经告警、正在做数据库选型评估的团队;想在不改业务代码的前提下做冷热数据分离的运维工程师;以及被分配了"把订单库拆了"这类任务、需要快速找到落地路径的开发者。下面我从分片模型、部署操作、BYMONTH 规则配置、常见坑和验收技巧五个层面展开,每一步都可以照着操作。
2. 先搞清楚 Mycat 的分片模型:三个配置文件和一条数据流
先别急着解压和启动,把 Mycat 的分片模型理清楚,后面改配置时才不会一头雾水。Mycat 本身不是一个存储引擎,它不真正保存数据,只负责把 SQL 路由到正确的物理 MySQL 上执行,然后汇总结果。
2.1 应用与 MySQL 之间的中间层,Mycat 到底代理了什么
Mycat 的本质是一个实现了 MySQL 协议的代理层。后端服务把 jdbc:mysql://mycat_host:8066/LOGIC_DB 当成一个普通 MySQL 实例来连接,Mycat 收到 SQL 后做三件事:解析 SQL、按分片规则决定去哪个物理节点执行、汇总结果返回给应用。对业务代码来说,它连的就是一个"大 MySQL",这个假象是 Mycat 最核心的价值。
代理层解决了两个实际问题。第一是连接收敛:后端几十个服务实例不会直接打爆 MySQL 的连接数,所有连接都落在 Mycat 的连接池上。第二是透明分片:逻辑表名在多个物理库中真实存在,Mycat 根据分片字段找到正确的物理表。分片规则越清晰,代理层的性能损耗越低;规则写得太模糊,Mycat 就会把所有分片都查一遍再合并结果,这种广播是最需要避免的。
理解这个代理模型还有一个实际意义:你的 SQL 最终会被真实执行在某个物理节点上,所以物理库的容量、慢查询、死锁这些指标,最终要回到 MySQL 侧去看,不能只盯着 Mycat 的监控页面。
2.2 schema.xml、rule.xml、server.xml 各管一段,谁也替代不了谁
Mycat 的配置集中在 conf 目录下的三个 XML 文件里。我见过不少团队只改其中一个却指望它生效,路由不对就开始怀疑中间件有 bug,实际上这三份文件的分工非常明确。
server.xml 管 Mycat 自己的连接账号、端口、系统参数。用什么用户名密码连接逻辑库,允许的最大连接数,全局序列采用哪种方案,都在这里定义。schema.xml 管逻辑库和物理库的映射:逻辑库里有几张逻辑表、每张表对应哪几个数据节点、每个数据节点指向哪台 MySQL 实例、读写是否分离。rule.xml 管分片规则:哪张表按哪个字段分片、用什么算法、算法参数怎么配。
三个文件的关系可以这么记:schema.xml 决定"有几张表、表在哪",rule.xml 决定"数据去哪张物理表",server.xml 决定"谁能连进来、连接资源怎么受限"。改完任何一份都要重启 Mycat 才生效,因为它们都是在启动时一次性加载的。我一般在改动 schema.xml 和 rule.xml 后,会先启动一个 console 模式的临时实例验证配置后再切流量,避免把线上实例搞挂。
2.3 分片字段的四个硬性条件,缺一个后面就等着重构
分片字段是整个分片方案里唯一不能拍脑袋决定的配置。它有四个硬性条件。
第一,分布够均匀。按月分片天然适合订单、日志这类按时间累积的数据,但如果是按用户 ID 分片,就要考虑高活跃用户是否会让某一个分片过热。第二,不允许更新。一旦某行数据的分片字段值被修改,它在逻辑上需要被移动到另一个物理分片,Mycat 对这种数据移动的支持非常有限,生产上基本只能靠应用层处理。第三,查询条件必须常带。路由能生效的前提是 SQL 的 where 条件里能提取到分片字段的值,如果业务查询经常不带这个字段,每次都是全分片广播,性能会比单库还差。第四,类型稳定。分片字段的类型和传参格式必须长期不变,改类型意味着所有历史数据的路由结果全部改变,这类重构在分库分表场景里代价极高。
这四个条件在选字段阶段就要全部过一遍,不要等上线后再回头改。按月分片之所以是大多数团队的第一选择,就是因为时间字段天然满足前三条,而第四条只要约定好日期格式就能控制住。
3. 把 mycat-1.6.7.6 跑起来的完整操作:解压、调 JVM、配数据节点
这一章从拿到 mycat-1.6.7.6_BYMONTH.zip 开始,一步步把它跑起来,并把三个真实 MySQL 数据节点配好。整个过程没有需要编译的代码,但有一些参数不调好,后面会反复出问题。
3.1 解压和 JVM 参数调整,启动前必须改的两处
发行包拿到后,先解压到固定目录。Mycat 解压后自带 bin、conf、lib、logs 等目录,不需要编译,只要有 Java 运行环境就能启动。
# 解压发行包到 /opt 目录,注意 zip 内自带 mycat 目录 unzip mycat-1.6.7.6_BYMONTH.zip -d /opt/ # 确认目录结构和 Java 环境 ls /opt/mycat/ java -version # 启动前修改 JVM 堆内存,默认 256M 在分片场景下撑不住 # 编辑 /opt/mycat/conf/wrapper.conf,至少调整为 2G/4G # wrapper.java.initmemory=2048 # wrapper.java.maxmemory=4096 # 先以 console 模式启动,日志实时打到终端,方便排错 /opt/mycat/bin/mycat console # 另开一个终端查看进程状态 /opt/mycat/bin/mycat statusmycat 启动脚本支持 start、console、stop、status 等参数。console 模式用于调试,退出终端进程就停;确认配置无误后,生产环境改用后台守护方式启动。wrapper.conf 里的两个堆内存参数分别控制初始堆和最大堆,分片节点越多、单条结果集越大,这两个值就要给得越足。我见过不少部署,表配置完全没问题,但因为堆内存默认值太小,运行几周后频繁 Full GC,路由性能急剧下降,误以为是 Mycat 的 bug。这类玄学问题,排查顺序永远是先看 JVM 再看配置。
3.2 用三个真实 MySQL 实例配置分片数据节点
这一步要准备三台可用的 MySQL 实例。它们可以分布在三台物理机上,也可以在同一台机器上跑三个不同端口的实例。分片的物理边界越独立,后面的水平扩展收益越大;如果只是把三个库放在同一台机器上,那解决的只是单表锁竞争,磁盘 IO 瓶颈还在。
<?xml version="1.0" encoding="UTF-8"?> <mycat:schema xmlns:mycat="http://io.mycat/"> <!-- 逻辑库名,应用 JDBC 连接时使用 --> <schema name="LOGIC_DB" checkSQLschema="true" sqlMaxLimit="100"> <!-- order_log 按 order_time 分片,三个数据节点 --> <table name="order_log" dataNode="dn1,dn2,dn3" rule="sharding_by_month"/> </schema> <!-- 数据节点:每个节点指定物理库名 --> <dataNode name="dn1" dataHost="hostA" database="logdb_01" /> <dataNode name="dn2" dataHost="hostB" database="logdb_02" /> <dataNode name="dn3" dataHost="hostC" database="logdb_03" /> <!-- 物理主机 A,balance=0 表示先不开启读写分离 --> <dataHost name="hostA" maxCon="100" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native"> <heartbeat>select user()</heartbeat> <writeHost host="mysqlA" url="192.168.10.11:3306" user="mcat" password="ChangeMe@123"/> </dataHost> <!-- hostB 与 hostC 的 dataHost 结构与 hostA 一致,按实际 IP 替换 --> <dataHost name="hostB" maxCon="100" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native"> <heartbeat>select user()</heartbeat> <writeHost host="mysqlB" url="192.168.10.12:3306" user="mcat" password="ChangeMe@123"/> </dataHost> <dataHost name="hostC" maxCon="100" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native"> <heartbeat>select user()</heartbeat> <writeHost host="mysqlC" url="192.168.10.13:3306" user="mcat" password="ChangeMe@123"/> </dataHost> </mycat:schema>schema 标签里声明了逻辑库 LOGIC_DB,table 标签的 dataNode 属性列出三个数据节点,rule 属性指向 rule.xml 里的规则名。dataNode 只是逻辑指针,真正连接到哪台 MySQL 由 dataHost 决定。writeHost 里的 url 指向物理 MySQL,账号需要有建表、读写和查询元数据的权限。这里 balance="0" 表示所有读写都走 writeHost,先把分片跑通再加 readHost,排查问题时少一个变量总是好的。
三个物理库名 logdb_01、logdb_02、logdb_03 需要在 MySQL 侧提前建好,Mycat 不会自动建库。建表 DDL 也必须到每个物理库手动执行,具体内容下一章会讲到。
3.3 EXPLAIN 验证分片路由,上线前必做的自检
配置是否生效,不要凭感觉,直接查路由。
-- 在 Mycat 逻辑库执行,观察输出里的节点信息 EXPLAIN SELECT * FROM order_log WHERE order_time = '2025-03-12';如果配置正确,EXPLAIN 输出的节点信息会指向 dn1、dn2、dn3 中的某一个;如果同时出现多个节点,说明这条 SQL 的条件没有被识别为分片字段,Mycat 走了全分片广播。这条命令是我每次改完 rule.xml 之后必做的第一个检查,它比任何日志都直观,展示的是 Mycat 内部真实的路由计算结果。
广播本身在少数场景下是可以接受的,比如按月分片后某些运营报表必须跨全月查询,但日常交易类 SQL 必须做到精确路由,否则分片不但没解决问题,还引入了一层代理开销。
4. BYMONTH 分片规则从 rule.xml 到建表语句:三处配置逐一攻破
这一章进入标题的核心:BYMONTH 按月分片的具体配置。很多文章只贴 rule.xml 就结束了,但实际落地时,物理库的 DDL 和节点数规划同样决定成败。
4.1 PartitionByMonth 算法原理:月份差取模,一句话就能讲透
Mycat 自带的按月分片算法是 PartitionByMonth,它的内部逻辑可以简化成一句话:把传入日期与基准日期的月份差算出来,对数据节点总数取模,结果就是目标分片下标。
具体来说,算法会解析日期字符串得到年、月,再与 sBeginDate 配置的起始年、月做差值。月份差等于 0 表示当月数据落在第 1 个节点,等于 1 落在第 2 个节点,依此类推。如果数据节点有 12 个,那么一年 12 个月刚好各占一个节点;第 13 个月到来时,月份差对 12 取模回到 0,数据再次写入第 1 个节点,循环往复。
这个算法的优势是简单、计算开销极小,路由只依赖日期字段,不需要查任何元数据表。代价是它不感知业务流量的波峰波谷:如果业务旺季是 3 月,那 3 月对应的那个分片永远是最热的,其他分片相对空闲。理解这一点很重要,因为它直接决定了下面节点数的配置策略。
4.2 按月分片的 tableRule、function 和物理表 DDL
rule.xml 是分片规则的唯一落点。配置分两层:tableRule 定义这张表的规则入口,function 定义实际执行分片的算法和参数。
<!-- tableRule:order_log 表按 order_time 字段进行分片 --> <tableRule name="sharding_by_month"> <rule> <columns>order_time</columns> <algorithm>partbyMonth</algorithm> </rule> </tableRule> <!-- function:使用 Mycat 自带的 PartitionByMonth --> <function name="partbyMonth" class="io.mycat.route.function.PartitionByMonth"> <property name="dateFormat">yyyy-MM-dd</property> <property name="sBeginDate">2024-01-01</property> </function>columns 是逻辑表的分片字段,必须与 schema.xml 里 table 的列名一致;algorithm 的名字要能对上 function 的 name 属性。dateFormat 是日期字符串的解析格式,决定了应用层传参能不能被正确识别。sBeginDate 是月份差计算的基准月,我强烈建议设置成业务最早可能出现数据的月份,而不是部署当天,原因在下一章的排坑里会详细说。
写好 rule.xml 后,还要到三个物理库执行建表语句。Mycat 分片只是路由层,物理表和索引必须自己在每个库建好,一个都不能少。
-- 在 logdb_01、logdb_02、logdb_03 三个物理库分别执行 CREATE TABLE order_log ( id BIGINT NOT NULL COMMENT '业务主键,由全局序列生成', order_time DATETIME NOT NULL COMMENT '下单时间,同时是分片字段', user_id BIGINT NOT NULL COMMENT '用户ID', amount DECIMAL(12,2) NOT NULL COMMENT '订单金额', PRIMARY KEY (id), KEY idx_order_time (order_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有一个常见误用:有人在第一个物理库建了临时表就让应用开始写入,结果同一张逻辑表在另外两个节点上根本不存在,路由过去后直接报 table doesn't exist。多分片环境里,DDL 变更脚本要写成循环执行的形式,每张物理表同步执行。这是我最早踩过的坑,也是分片环境运维和单库运维最不一样的地方。
4.3 节点数怎么定:数据分布和循环周期的权衡
PartitionByMonth 的取模空间就是数据节点数,所以节点数直接决定两件事:一是每个分片最多积累多少数据量,二是数据循环回退的周期多长。
如果节点数设为 12,每个分片承载一个月的数据,13 个月后新数据写回第 1 个节点,第 1 个节点里就有两个完整月份的数据。如果业务数据量大,这样的循环会让某些分片持续膨胀,最终比单表还难维护。常见的做法是节点数设置为 12 的整数倍,比如 24 或 36,让每个分片只承载半个月或三分之一月的数据;或者保持一月一节点,但配合归档任务把超过 12 个月的物理分片从逻辑配置中摘掉。
另一个决策点是物理分片用节点下标还是业务月份命名。我的偏好是用 dn1、dn2 这类哑编号命名 dataNode 和物理库,把"dn1 代表哪个月的数据"完全交给规则计算,而不是把库名写成 logdb_202401。这样调整节点数或者迁移分片时,不用改物理库名,只改 schema.xml 的映射即可,灵活性高很多。
5. 按月分片排坑与常见问题:最容易翻车的 5 个场景
这一章把我见到过的、以及自己踩过的坑整理成现象、原因、解决三步,按严重程度排序。每一条都对应一个真实发生过的问题,照着检查能省下不少半夜回滚的时间。
5.1 日期格式不匹配,点查变成全分片广播
现象:一条按天点查的 SQL 本应只访问一个分片,慢查询日志里却出现三个物理库同时执行同一条 SQL。
原因:应用层传的日期字符串是 2025/03/12,rule.xml 里 dateFormat 配置的是 yyyy-MM-dd,PartitionByMonth 解析失败后被迫退回全分片广播。
解决:统一应用传参与 dateFormat 的格式,包括时间段查询的边界是闭区间还是开区间,在第一次对接时就约定好。改完 rule.xml 需要重启 Mycat 生效,这类约定最好写进开发规范里,让所有团队共用一套日期格式。
5.2 sBeginDate 起始月设成部署当月,历史数据路由错乱
现象:上线半年后,补录的历史订单查询时用 explain 发现路由到了错误的节点,直接查不到数据。
原因:sBeginDate 设成了部署当月,导致所有更早日期的月份差为负数。负数取模在 Java 里可能是负下标,Mycat 找不到对应数据节点,报错或者路由错乱都出现过。
解决:sBeginDate 必须取业务最早可能出现数据的那个自然月,宁可早一个月也不要晚。数据迁移上线时,先查一下源表的最小日期,再决定这个值填什么。
5.3 分片字段为 NULL,写入直接报 can't find datanode
现象:某个写入入口没有传 order_time,insert 语句执行时报 can't find any valid datanode。
原因:路由阶段拿不到可解析的日期值,PartitionByMonth 无法计算目标分片下标,这条 SQL 就失去了路由依据。
解决:物理表 DDL 将分片字段设为 NOT NULL,应用层在写入前做默认值兜底。同时排查这个入口为什么没传值,通常背后是一个被忽略的接口字段缺失,兜底只是防御手段。
5.4 多表 JOIN 分片键不一致,跨节点聚合失败
现象:order_log 与 order_detail 关联查询时,Mycat 报跨节点关联不支持,或者查询变得极慢。
原因:两张表的分片字段或者分片规则不一致,同一笔订单的主表和明细可能落到不同物理节点,Mycat 无法在本地完成 JOIN。
解决:要么把明细表的分片字段也设为 order_time,保证同月份数据在同一个节点;要么使用 Mycat 的 ER 关系配置,把 order_detail 配置成 order_log 的 childTable。前者改动最小,适合订单这种按时间访问的场景;后者适合明细表必须独立分片的场景。
5.5 漏配全局序列,三个分片的主键全部从 1 开始
现象:插入数据没有报错,但后续查询发现逻辑主键重复,多个物理表中都出现 id=1 的记录。
原因:每个物理 MySQL 的 auto_increment 各自独立,从 1 开始递增,分片后主键必然冲突。
解决:在 server.xml 里配置全局序列,Mycat 支持本地时间戳、数据库、自定义等多种方案。对订单表这种需要回显主键的场景,我一般用数据库方式保证严格递增;对日志类不关心主键顺序的场景,本地时间戳方式就够用。要注意逻辑表的主键生成必须交给 Mycat 处理,应用不能再自行传 id,否则序列配置形同虚设。
6. 进阶验证技巧:给按月分片数据做个体检
配置全部落地后,别急着庆祝。我给自己定了一条规矩:每套分片方案上线前,都要跑完三遍体检,确认之后再让业务流量进来。
体检第一项是路由精确性检查。把三种代表性 SQL 用 EXPLAIN 各跑一遍:按月点查、按月范围查询、不带分片字段的查询。前两种必须路由到单节点,第三种如果广播,要明确这是有意的全量查询还是漏了条件。
体检第二项是分片数据量分布检查。登录三个物理库分别执行下面这条 SQL,对比各分片的行数和容量:
-- 分别在三个物理库执行,对比分片数据分布 SELECT 'logdb_01' AS db_name, COUNT(*) AS row_count, ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = 'logdb_01' AND table_name = 'order_log' UNION ALL SELECT 'logdb_02', COUNT(*), ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) FROM information_schema.tables WHERE table_schema = 'logdb_02' AND table_name = 'order_log' UNION ALL SELECT 'logdb_03', COUNT(*), ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) FROM information_schema.tables WHERE table_schema = 'logdb_03' AND table_name = 'order_log';按月分片天然允许各节点数据量不均衡,这里要看的是有没有某个分片远超容量阈值。如果 3 月是旺季,dn3 数据量是 dn1 的两倍属于预期;但如果某个分片已经逼近物理磁盘容量,就该考虑增加节点或者把历史分片归档出去了。
体检第三项是一致性抽查。对近三个月的订单,从逻辑库按 id 查一条,再直连对应物理库按相同条件查一条,比对结果一致。这一步不复杂,但能发现隐藏的配置错误。
最后讲一个自己的教训作为收尾:最早做分片时,我随手把 sBeginDate 填成部署当天,三个月后补录历史数据全部路由错乱,那次是连夜回滚才扛过去。之后我把 rule.xml 里每个属性的取值逻辑都写进了上线前检查单,再也没有因为分片配置翻过车。希望帮到你。
本文还有配套的精品资源,点击获取