ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MyCat按月分表实战:BYMONTH定制版原理与生产避坑指南

MyCat按月分表实战:BYMONTH定制版原理与生产避坑指南

简介:本资源是基于MyCat 1.6.7.6正式版深度定制的分表增强版,专为需要按月动态分表(subTables)的Java后端开发与数据库中间件运维人员设计,解决传统MyCat不支持正则化、起始时间驱动的BYMONTH分表配置难题。压缩包共111个文件,含53个核心jar包(如Mycat-server-1.6.7.6-release.jar、druid-1.0.26.jar、curator-client-4.0.1.jar等)、18个properties配置文件、10个xml配置模板及4个Linux启动脚本(sh),完整覆盖服务部署、分片规则定义、连接池与日志模块;整体包体24.86MB,轻量易集成。目前已有375人学习下载。用户可直接获取开箱即用的按月分表能力:通过schema.xml中subTables="tableName_$202101-?"与subTableWay="BYMONTH"即可实现从指定起始月自动映射至当前月的子表路由,配合预置的SQL建表脚本与动态建表逻辑,显著降低分表运维成本。

1. MyCat 1.6.7.6 BYMONTH 版本不是“官方补丁”,而是实测可用的按月分表生产级定制包

你手头正跑着一个日增百万级订单的电商后台,MySQL 单表已超 2800 万行,order_info查询开始抖动,ALTER TABLE加个索引要锁表 47 分钟——这时候翻 MyCat 官方文档,发现subTables只支持按mod、hash、range等静态规则分片,压根没提“按年月自动建表、自动路由到order_202401/order_202402”这种动态能力。而这个mycat-1.6.7.6_BYMONTH.zip,就是某公司线上稳定运行 14 个月的实战产物:它在原生 MyCat 1.6.7.6 基础上,重写了TableRule解析器与RouterUtil路由核心,让subTables="order_${yyyyMM}"这种写法真正生效,且支持 INSERT 自动创建目标月表、SELECT 自动下推时间范围、DELETE 按月精准清理——不是 demo,不是 PoC,是带完整 SQL 解析日志、分片键校验、跨月查询兜底策略的生产就绪包。适合正在被时间维度分表卡住脖子的中型业务系统,尤其当你已用sharding-jdbc试过但发现其StandardShardingAlgorithm对BETWEEN '2024-01-01' AND '2024-03-31'这类查询下推不干净时,这个包能直接切过去跑通。


2. 按月分表不是加个配置就行:从源码层理解 BYMONTH 的三个关键改造点

MyCat 原生分片逻辑走的是AbstractPartitionAlgorithm → RuleAlgorithm → calculate()这条链,而subTables仅用于生成物理表名列表,不参与路由计算。BYMONTH 版本之所以能工作,是因为它在三个关键节点做了侵入式增强。下面逐层拆解,不贴大段源码,只聚焦你部署时必须看懂的逻辑分支。

2.1 分片键识别:MonthBasedColumnParse替代默认ColumnParse

原版 MyCat 对WHERE create_time > '2024-02-01'这类条件,只会提取出create_time字段名,但不会解析其值是否为日期格式、是否可转为yyyyMM。BYMONTH 新增了MonthBasedColumnParse类,在SQLParser解析 AST 后,对WHERE子句中所有Timestamp/Date/String类型字段做二次扫描:

// 源码路径:io.mycat.route.parser.MonthBasedColumnParse.java public class MonthBasedColumnParse extends ColumnParse { @Override public void parseCondition(String columnName, Object value, List<String> monthList) { if (isDateColumn(columnName)) { String yyyyMM = parseToYYYYMM(value); // 支持 '2024-02-15', '20240215', '1706774400000' 三种输入 if (yyyyMM != null) { monthList.add(yyyyMM); } } } }

提示:该解析器默认只认create_time、update_time、order_date三个字段名(可配置),若你的分片键叫gmt_create,需在schema.xml中显式声明<property name="dateColumn">gmt_create</property>,否则路由会 fallback 到全表扫描。

2.2 表名生成器:MonthSubTableHandler动态拼接而非静态枚举

原版subTables="t1,t2,t3"是预加载字符串数组,而 BYMONTH 的MonthSubTableHandler在每次路由前实时生成:

<!-- schema.xml 中的 table 定义 --> <table name="order_info" dataNode="dn1,dn2,dn3" rule="byMonth" subTables="order_${yyyyMM}" dateColumn="create_time" monthFormat="yyyyMM" />

对应 Java 层调用链为:
RouteStrategy → getRouteResult() → MonthSubTableHandler.getSubTableName()→ 根据create_time条件值(如'2024-03-10')→ 计算出order_202403→ 再查dataNode映射确认该表落在dn2上。

关键参数说明:

  • subTables="order_${yyyyMM}":${}内必须为yyyyMM、yyyy、yyyyMMDD三者之一,不支持自定义格式(如yyMM会报错)
  • monthFormat="yyyyMM":与${}中格式严格一致,不一致会导致路由失败
  • dateColumn:必须与MonthBasedColumnParse识别的字段名完全匹配(大小写敏感)

2.3 跨月查询兜底:MultiMonthRouter实现BETWEEN和IN的智能拆分

当 SQL 出现WHERE create_time BETWEEN '2024-01-15' AND '2024-03-20'时,原版 MyCat 会因无法确定单一分片键值而路由到全部 dataNode。BYMONTH 的MultiMonthRouter会:

  1. 解析BETWEEN边界 → 得到起始月202401、结束月202403
  2. 生成月列表["202401","202402","202403"]
  3. 对每个yyyyMM,构造子查询SELECT * FROM order_202401 WHERE create_time >= '2024-01-15'(自动加时间过滤下推)
  4. 合并结果集并去重(若开启needMerge=true)

注意:此机制仅对BETWEEN、>= AND <=、IN ('2024-01-01','2024-02-01')生效;LIKE '2024%'或DATE_FORMAT(create_time,'%Y%m')='202401'不会被识别,将触发全节点广播。


3. 配置不是复制粘贴:schema.xml与rule.xml的五处硬编码约束

很多开发者解压后直接覆盖conf/目录,启动报NoSuchMethodError或TableRule not found,问题往往出在配置文件与 BYMONTH 版本的强耦合上。以下五处必须手工核对,不能依赖旧配置迁移。

3.1schema.xml中table标签的强制属性

原版 MyCat 的table标签允许省略subTables,但 BYMONTH 要求所有启用按月分表的 table 必须显式声明subTables、dateColumn、monthFormat,缺一不可:

<!-- ✅ 正确写法 --> <table name="order_info" dataNode="dn1,dn2,dn3" rule="byMonth" subTables="order_${yyyyMM}" dateColumn="create_time" monthFormat="yyyyMM" /> <!-- ❌ 错误写法(缺少 dateColumn) --> <table name="order_info" dataNode="dn1,dn2,dn3" rule="byMonth" subTables="order_${yyyyMM}" /> <!-- 启动时抛异常:MonthSubTableHandler requires 'dateColumn' property -->

3.2rule.xml中function的 class 名必须精确匹配

BYMONTH 新增了两个核心类,rule.xml中function的class属性必须一字不差:

<!-- rule.xml --> <function name="byMonth" class="io.mycat.route.function.MonthPartionAlgorithm" /> <!-- 注意:不是 MonthPartitionAlgorithm(少个 'i' 就报错) --> <!-- 也不是 io.mycat.route.function.PartitionByMonth(这是旧版社区魔改包) -->

同时,该function下的property必须包含dateFormat(注意不是datePattern):

<function name="byMonth" class="io.mycat.route.function.MonthPartionAlgorithm"> <property name="dateFormat">yyyy-MM-dd HH:mm:ss</property> <!-- 此格式仅用于解析字符串型日期,不影响 subTables 生成 --> </function>

3.3dataNode定义必须与物理库表名前缀一致

假设你的 MySQL 物理库名为db_order_202401、db_order_202402,则dataNode的database属性必须为db_order_${yyyyMM}:

<dataNode name="dn1" dataHost="host1" database="db_order_${yyyyMM}" /> <!-- 若写成 database="db_order",则路由到 dn1 时会尝试连 db_order.order_202401,但实际库名是 db_order_202401 --> <!-- 导致报错:Unknown database 'db_order' -->

3.4sequence配置必须禁用或重写

BYMONTH 默认关闭sequence全局序列(因其与按月分表冲突),若你在server.xml中启用了<system><property name="sequnceHandlerType">2</property></system>,必须注释掉:

<!-- server.xml --> <!-- <system> --> <!-- <property name="sequnceHandlerType">2</property> --> <!-- </system> --> <!-- 否则启动时报:SequenceHandler not compatible with MonthSubTableHandler -->

如需自增 ID,应改用 MySQL 的AUTO_INCREMENT+REPLACE INTO模式,或在应用层生成雪花 ID。

3.5user权限需显式授予跨库查询权限

由于BETWEEN查询会路由到多个dataNode,MyCat 需要以同一账号连接所有物理库。若dn1用user1、dn2用user2,则跨月查询会因权限不足中断:

<!-- user.xml --> <user name="app_user"> <property name="password">mypass</property> <property name="schemas">db_order</property> <!-- 必须确保 app_user 在所有物理库(db_order_202401, db_order_202402...)中都存在且有 SELECT/INSERT 权限 --> </user>

验证命令(在任一物理 MySQL 中执行):

SELECT user, host FROM mysql.user WHERE user='app_user'; -- 应返回多行,host 包含 '%' 或具体 IP SHOW GRANTS FOR 'app_user'@'%'; -- 必须包含 GRANT SELECT, INSERT ON `db_order\_%`.* TO 'app_user'@'%'

4. 避坑:生产环境踩过的五个真实问题与速查方案

别等上线后半夜被报警电话叫醒。以下是某公司用该包支撑双十一流量峰值时,记录在案的五个高频故障,每一条都附带现象 → 原因 → 解决的闭环排查路径。

4.1 现象:INSERT 成功但数据未写入任何物理表,日志无报错

原因:dateColumn值为NULL或非法字符串(如'0000-00-00'),MonthBasedColumnParse.parseToYYYYMM()返回null,导致subTableName生成为空,最终路由到nulldataNode。
解决:

  • 在应用层强制校验create_time非空且格式合法
  • 在 MyCat 日志中搜索getSubTableName return null关键字定位问题 SQL
  • 临时兜底:在schema.xml中为该table添加defaultNode="dn1"属性,使空值路由到默认节点

4.2 现象:SELECT * FROM order_info WHERE create_time = '2024-02-15'返回空,但order_202402表中明明有数据

原因:monthFormat="yyyyMM"与dateFormat="yyyy-MM-dd"冲突。parseToYYYYMM('2024-02-15')得到202402,但subTables="order_${yyyyMM}"生成order_202402,而物理表实际名为order_202402—— 看似没问题?错!问题出在dataNode.database="db_order_${yyyyMM}",若物理库名为db_order_202402,则database属性必须为db_order_${yyyyMM},但subTables与database的${yyyyMM}必须由同一DateFormat解析。若dateFormat设为yyyy-MM-dd,则database解析出db_order_202402,但subTables解析出order_202402,二者匹配;若dateFormat设为yyyyMMdd,则subTables解析出order_20240215(错误),导致找不到表。
解决:统一dateFormat为yyyy-MM-dd,并确保subTables和database中的占位符均为${yyyyMM}(非${yyyyMMdd})。

4.3 现象:DELETE FROM order_info WHERE create_time < '2024-01-01'执行极慢,CPU 占用 100%

原因:BYMONTH 对DELETE语句的跨月处理采用“逐月生成子 DELETE + 串行执行”策略。若条件覆盖 36 个月(2021-2024),则生成 36 条DELETE FROM order_202101 WHERE ...并依次执行,期间 MyCat 连接池被占满。
解决:

  • 业务侧改为按月分批删除:DELETE ... WHERE create_time >= '2021-01-01' AND create_time < '2021-02-01'
  • 或在物理库层面执行DROP TABLE order_202101(更高效,但需确保无长事务)
  • 修改server.xml中<system><property name="processors">8</property></system>提升并发数(默认 4)

4.4 现象:SELECT COUNT(*) FROM order_info WHERE create_time > '2024-03-01'结果比 MySQL 直查少 23%

原因:COUNT(*)聚合未下推到各月表,而是 MyCat 拉取所有匹配行后在内存中计数,当单月数据超 50 万行时触发bufferPool溢出,部分结果被丢弃。
解决:

  • 强制使用SELECT COUNT(*) FROM (SELECT 1 FROM order_info WHERE create_time > '2024-03-01') t触发子查询优化
  • 或在server.xml中调大<system><property name="bufferPoolChunkSize">4096</property></system>(默认 1024)
  • 终极方案:改用SELECT SUM(cnt) FROM (SELECT COUNT(*) cnt FROM order_202403 UNION ALL SELECT COUNT(*) cnt FROM order_202404 ...) t

4.5 现象:MyCat 启动后show @@datanode显示dn1状态为INITING,30 秒后变OFFLINE

原因:dataNode.database="db_order_${yyyyMM}"中的${yyyyMM}在启动时无法解析(无上下文日期),导致连接字符串为jdbc:mysql://host:3306/db_order_${yyyyMM},MySQL 拒绝连接。
解决:

  • 启动前手动创建至少一个物理库,如CREATE DATABASE db_order_202401
  • 或修改dataNode为固定库名database="db_order_init",待 MyCat 启动成功后,再通过mysql -h127.0.0.1 -P8066 -uapp_user -pmypass -e "create schema db_order_202401"创建月库
  • 血泪经验:首次部署务必先停掉所有应用,用mysql -h127.0.0.1 -P8066连上 MyCat,执行show @@datanode确认全绿再放流量

5. 验证不是跑个 SELECT 就完事:四步法确认按月分表真正生效

配完schema.xml和rule.xml,很多人只执行SELECT * FROM order_info LIMIT 10就认为 OK。但真正的“生效”,必须满足四个技术指标:路由精准、建表自动、跨月可控、性能不降。下面是我每次上线前必做的四步验证清单,漏一步都可能埋雷。

5.1 第一步:用explain确认路由节点与预期完全一致

MyCat 1.6.7.6 BYMONTH 支持explain sql查看真实路由计划。这不是模拟,是实际执行前的决策快照:

-- 连接 MyCat(端口 8066) mysql -h127.0.0.1 -P8066 -uapp_user -pmypass -- 执行 explain EXPLAIN SELECT * FROM order_info WHERE create_time = '2024-04-10';

预期输出:

DATA_NODE SQL dn2 SELECT * FROM order_202404 WHERE create_time = '2024-04-10'

关键检查点:

  • DATA_NODE必须是单个节点(如dn2),不能是dn1,dn2,dn3(说明未识别到日期)
  • SQL中的表名必须为order_202404(与create_time的2024-04匹配),不能是order_${yyyyMM}(说明subTables未生效)
  • 若出现broadcast字样,立即检查dateColumn是否拼写错误

5.2 第二步:触发自动建表,验证物理库中表结构与 MyCat 一致

BYMONTH 的自动建表功能在首次INSERT时触发。但很多人 INSERT 后查物理库,发现表不存在——因为 MyCat 默认异步建表,且需满足两个前提:

  1. INSERT语句中dateColumn值必须非空且可解析(见 4.1)
  2. 物理库需有CREATE TABLE权限(GRANT CREATE ON db_order_202404.* TO 'app_user'@'%')

验证脚本(在 MyCat 中执行):

-- 插入测试数据(确保 create_time 为未来月份,避免与现有数据混淆) INSERT INTO order_info(id, user_id, amount, create_time) VALUES (999999, 1001, 99.9, '2025-01-01'); -- 等待 3 秒,查物理库(假设 dn1 对应 host1:3306) -- 在 host1 的 MySQL 中执行: SHOW CREATE TABLE db_order_202501.order_202501;

预期结果:

  • 表结构必须与schema.xml中order_info的createTableSQL完全一致(包括 ENGINE、CHARSET、COMMENT)
  • 若createTableSQL未定义,则表结构为 MyCat 内置模板(id BIGINT PRIMARY KEY, ...),此时需手动补全索引

5.3 第三步:跨月查询压力测试,监控processor线程与 GC

BETWEEN查询是性能黑洞高发区。用sysbench或自写脚本模拟真实流量:

# 模拟 50 并发,查询 3 个月数据 for i in {1..50}; do mysql -h127.0.0.1 -P8066 -uapp_user -pmypass -e " SELECT COUNT(*) FROM order_info WHERE create_time BETWEEN '2024-02-01' AND '2024-04-30'; " & done wait

监控指标(在 MyCat 启动目录执行):

  • tail -f logs/mycat.log | grep "route to":确认是否真的只路由到dn1,dn2,dn3(3 个节点),而非dn1,dn2,...,dn10(说明月范围计算错误)
  • jstat -gc $(pgrep -f "MyCatServer") 1s:观察GCT(GC 时间)是否持续 > 200ms,若是,调大-Xmx4g(默认 2g)
  • netstat -anp | grep :8066 | wc -l:连接数不应超过server.xml中<system><property name="processors">8</property></system>× 100

5.4 第四步:对比 MyCat 与直连 MySQL 的执行计划差异

最致命的坑是:MyCat 路由正确,但物理 SQL 执行效率暴跌。原因往往是WHERE条件未下推,或索引失效。必须用EXPLAIN对比:

-- 在 MyCat 中执行 EXPLAIN SELECT * FROM order_info WHERE create_time > '2024-03-01' AND user_id = 1001; -- 在物理库 db_order_202403 中执行(替换为真实库名) EXPLAIN SELECT * FROM order_202403 WHERE create_time > '2024-03-01' AND user_id = 1001;

关键比对项:

指标MyCat EXPLAIN物理库 EXPLAIN是否正常
typeref/rangeref/range若 MyCat 是ALL,说明user_id索引未被识别
keyidx_user_ididx_user_id若 MyCat 为空,检查schema.xml中table是否配置needAddLimit="false"
rows12001200若 MyCat 是 120000,说明条件未下推,需加force push downhint

我的习惯:每次上线新分表规则,我都会在测试库跑一遍这四步,并把EXPLAIN截图存档。从那以后我每次改subTables配置,都强制走一遍explain → insert → between → explain四步闭环,宁可多花 20 分钟,也不愿凌晨三点修线上慢查询。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进