
1. 分库场景下 MyBatis 与 MyCat 的协作边界分库这件事很多团队第一次做的时候都会把注意力全放在“怎么把数据切开”上结果真正上线之后才发现最麻烦的不是切分规则本身而是应用层和中间件之间的职责怎么划分。MyBatis 作为 ORM 框架负责的是 SQL 映射、参数绑定、结果集封装MyCat 作为数据库中间件负责的是 SQL 解析、路由计算、结果归并。两者配合的时候如果边界没划清楚就会出现“应用层以为中间件会处理中间件以为应用层会规避”的尴尬局面。我见过不少项目在整合初期直接把原来的单库 MyBatis 配置原封不动指向 MyCat然后发现分页查询结果不对、批量插入报错、自增主键拿不到。这些问题的根源基本都出在没有理解 MyCat 对 SQL 的解析能力和限制上。MyCat 并不是一个透明的代理它对 SQL 有明确的语法要求尤其是分片键相关的查询、跨库 JOIN、子查询、UNION 这些操作都有各自的处理逻辑和性能代价。所以这一章我想先把协作边界讲清楚。MyBatis 侧需要做的是确保生成的 SQL 对 MyCat 友好比如分页语句的写法、批量操作的拆分方式、主键生成策略的选择。MyCat 侧需要做的是配置好 schema、rule、dataNode 这些核心文件让路由规则和实际数据分布一致。两边任何一边偷懒最后都会在联调阶段加倍还回来。1.1 为什么不是“配好数据源就完事”很多人觉得整合就是改个 JDBC URL把原来连 MySQL 的地址换成 MyCat 的地址就行。这个想法在只做读写分离的时候勉强成立但一旦涉及分库分表就完全不够了。原因在于 MyCat 需要知道你的分片规则而分片规则又和你的业务查询模式强相关。比如你按用户 ID 取模分片那么所有不带用户 ID 的查询都会变成全库扫描MyCat 会把 SQL 发到所有分片节点再归并结果。这种查询在单库时代可能只要 50ms到了分库环境可能变成 500ms 甚至更久。MyBatis 这边如果还在用SELECT * FROM order WHERE status 1这种没有分片键的语句那 MyCat 就只能广播。广播本身不是错误但你要清楚它的代价。我一般建议在整合初期就把所有 Mapper 里的 SQL 过一遍标出哪些是分片键查询、哪些是维度查询、哪些是统计查询然后针对不同类型决定是走 MyCat 还是走其他路径。1.2 MyCat 对 MyBatis 生成 SQL 的隐性约束MyBatis 的动态 SQL 很灵活if、choose、foreach这些标签能拼出各种形态的语句。但 MyCat 的 SQL 解析器不是万能的它对某些语法结构的支持有限。比如多层嵌套的子查询、复杂的窗口函数、某些类型的 JOINMyCat 可能无法正确路由或者直接报解析错误。我在实际项目里遇到过最典型的一个坑是INSERT INTO ... SELECT ...这种语句。MyBatis 里用selectKey配合批量插入的时候生成的 SQL 在单库没问题但 MyCat 对这类语句的路由逻辑和普通 INSERT 不一样导致数据被写到了错误的分片。后来改成逐条插入或者用 MyBatis 的ExecutorType.BATCH才解决。这个后面会详细讲。还有一个容易被忽略的点是大小写。MyCat 的 schema 配置里表名和字段名的大小写敏感性和 MySQL 本身可能不一致如果 MyBatis 的映射文件里写的是小写而 MyCat 配置里是大写路由就可能匹配不上。这种问题在联调时往往表现为“表不存在”或者“字段不存在”但实际数据库里明明有。2. MyCat 核心配置文件的落地细节MyCat 的配置体系围绕三个核心文件展开server.xml、schema.xml、rule.xml。这三个文件的关系可以这样理解server.xml定义的是“谁能连进来”schema.xml定义的是“数据怎么分布”rule.xml定义的是“按什么规则分”。三者必须严格对应任何一处不一致都会导致路由失败。2.1 server.xml 里那些容易配错的参数server.xml里最关键的几个配置项是user、schema、readOnly和privileges。user标签里的name和password是应用连接 MyCat 时用的凭据不是连接后端 MySQL 的凭据。后端 MySQL 的连接信息是在schema.xml的dataNode里配的。这个区分很重要因为很多新手会把两套凭据搞混。schema标签里的name属性要和schema.xml里的schema名称一致否则 MyCat 启动时会报找不到 schema。readOnly属性如果设成 true那么所有写操作都会被拒绝这个在只读从库场景下有用但在分库写入场景下必须设成 false。还有一个参数是defaultAccount它决定了没有显式指定账户时的默认行为。我一般建议显式配置不要依赖默认值因为默认值在不同版本里可能有差异。user nameapp_user defaultAccounttrue property namepasswordapp_password/property property nameschemasorder_db/property property namereadOnlyfalse/property /user上面这段配置的意思是应用用app_user和app_password连接 MyCat可以访问order_db这个逻辑库并且允许写操作。注意这里的order_db是逻辑库名不是物理库名。2.2 schema.xml 中 dataNode 与 dataHost 的映射逻辑schema.xml是 MyCat 配置里最核心也最容易出错的部分。它的结构是schema包含多个table每个table指定dataNode每个dataNode又关联dataHost和具体的物理库。这个层级关系必须清晰。schema nameorder_db checkSQLschemafalse sqlMaxLimit100 table namet_order dataNodedn_order_0,dn_order_1 rulemod-long / /schema dataNode namedn_order_0 dataHosthost_master databaseorder_0 / dataNode namedn_order_1 dataHosthost_master databaseorder_1 / dataHost namehost_master maxCon100 minCon10 balance0 writeType0 dbTypemysql dbDrivernative heartbeatselect user()/heartbeat writeHost hosthostM1 url127.0.0.1:3306 userroot passwordroot /writeHost /dataHost这里有几个细节值得展开。checkSQLschema如果设成 trueMyCat 会把 SQL 里的 schema 前缀去掉再路由这个在跨 schema 查询时有用但一般建议设成 false避免意外行为。sqlMaxLimit是默认返回行数限制如果不设MyCat 默认返回 100 条这个在分页查询时特别容易踩坑。dataHost里的balance参数决定读操作的负载均衡策略writeType决定写操作的路由策略。在分库场景下如果每个分片都是独立的主库balance设成 0 就行因为不需要读写分离。heartbeat语句用来检测后端 MySQL 是否存活一般用select user()或者select 1都可以。2.3 rule.xml 分片算法的选择与参数计算rule.xml定义分片算法。MyCat 内置了十几种算法常用的有mod-long取模、hash-int哈希取整、range-long范围分片、sharding-by-date按日期分片等。选择哪种算法取决于你的业务查询模式和数据增长趋势。取模算法最简单数据分布也最均匀但扩容时需要迁移大量数据。范围分片扩容方便但容易出现热点。按日期分片适合时间序列数据但需要定期维护分片。tableRule namemod-long rule columnsuser_id/columns algorithmmod-long/algorithm /rule /tableRule function namemod-long classio.mycat.route.function.PartitionByMod property namecount2/property /function上面这个配置的意思是按user_id对 2 取模余数 0 的落到第一个 dataNode余数 1 的落到第二个。count的值必须和dataNode的数量一致否则会出现路由到不存在的节点。这里有个经验count最好设成 2 的幂次比如 2、4、8、16。因为取模运算在 2 的幂次下可以用位运算优化性能更好。而且后续如果要做一致性哈希扩容2 的幂次也更容易处理。3. MyBatis 侧的适配改造与 SQL 写法调整MyBatis 这边要做的改造核心思路是“让生成的 SQL 对 MyCat 友好”。具体来说就是避免那些 MyCat 不支持或者支持不好的语法同时利用 MyBatis 的特性来配合分片规则。3.1 数据源配置从单库切换到 MyCat数据源切换本身很简单就是把 JDBC URL 从jdbc:mysql://127.0.0.1:3306/order_db改成jdbc:mysql://127.0.0.1:8066/order_db。8066 是 MyCat 的默认服务端口9066 是管理端口。但这里有几个细节要注意。第一驱动类还是 MySQL 的驱动因为 MyCat 兼容 MySQL 协议。第二连接池的参数可能需要调整因为 MyCat 本身也有连接池两层连接池叠加可能导致连接数膨胀。我一般建议应用侧的连接池最大连接数设成 MyCat 侧maxCon的 60% 到 70%留出余量。bean iddataSource classcom.zaxxer.hikari.HikariDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver / property namejdbcUrl valuejdbc:mysql://127.0.0.1:8066/order_db?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalse / property nameusername valueapp_user / property namepassword valueapp_password / property namemaximumPoolSize value60 / property nameminimumIdle value10 / property nameconnectionTimeout value30000 / /bean注意 URL 里的useSSLfalse因为 MyCat 默认不支持 SSL 连接如果开着 SSL 会连接失败。characterEncodingutf8也要加上避免中文乱码。3.2 分页查询在 MyCat 下的正确写法分页是分库场景下最容易出问题的地方。单库时代MyBatis 的LIMIT offset, size直接发给 MySQL 就行。但在 MyCat 下如果查询不带分片键MyCat 需要把 SQL 发到所有分片每个分片返回offset size条然后在 MyCat 层做归并排序最后取offset到offset size之间的数据。这个代价随分片数量线性增长。更麻烦的是如果排序字段不是分片键MyCat 的归并排序可能和 MySQL 的排序结果不一致导致分页数据重复或丢失。我遇到过最典型的情况是按创建时间倒序分页第一页和第二页有重复数据。原因是 MyCat 从各个分片取数据后做堆排序但堆排序不是稳定排序相同创建时间的记录顺序不确定。解决办法有两个一是排序字段加上唯一键作为第二排序条件比如ORDER BY create_time DESC, id DESC这样排序结果就稳定了。二是尽量让分页查询带上分片键这样 MyCat 可以直接路由到单个分片分页逻辑就和单库一样了。select idselectByPage resultMapBaseResultMap SELECT * FROM t_order WHERE user_id #{userId} ORDER BY create_time DESC, id DESC LIMIT #{offset}, #{size} /select上面这个查询带了user_idMyCat 可以直接路由到对应分片分页效率最高。如果业务上确实无法带分片键那就只能接受全库扫描的代价同时用sqlMaxLimit限制最大返回行数避免内存溢出。3.3 批量插入与自增主键的处理策略批量插入在分库场景下需要特别注意。MyBatis 的foreach批量插入生成的 SQL 是INSERT INTO t_order (col1, col2) VALUES (v1, v2), (v3, v4)。MyCat 对这条 SQL 的处理方式是根据每一行的分片键值把数据拆分到不同的分片。如果所有行的分片键值都落在同一个分片那就直接发过去如果落在不同分片MyCat 会拆成多条 INSERT 分别发送。这个行为本身没问题但性能上不如应用层自己拆分。因为 MyCat 的拆分是在 SQL 解析阶段做的有一定的 CPU 开销。如果批量插入的数据量很大比如几千条建议在应用层按分片键分组然后分组批量插入。自增主键是另一个坑。单库时代MyBatis 可以用useGeneratedKeystrue和keyProperty拿到自增 ID。但在 MyCat 下如果分片规则是按自增 ID 取模那么 MyCat 需要自己生成全局唯一 ID而不是依赖后端 MySQL 的自增。MyCat 支持多种全局序列生成方式包括本地文件、数据库、时间戳等。system property namesequnceHandlerType2/property /systemsequnceHandlerType设成 2 表示使用数据库方式生成全局序列。这种方式需要在 MyCat 的配置库里建一张MYCAT_SEQUENCE表然后为每个需要全局序列的表插入一行配置。这种方式的好处是 ID 连续缺点是每次生成都要查数据库性能有瓶颈。如果追求性能可以用时间戳方式但 ID 会很长而且不是严格递增。我一般建议如果业务对 ID 连续性没要求用时间戳方式最省事如果要求连续用数据库方式但要加缓存比如一次取 1000 个 ID 缓存在 MyCat 内存里用完再取。4. 联调阶段的高频故障与排查链路整合完成之后真正的挑战才开始。联调阶段遇到的问题往往不是配置写错了而是配置之间的相互作用导致的。这一章我按故障现象来组织每个现象给出排查链路和修复方案。4.1 查询结果为空但数据库明明有数据这个现象最让人抓狂。应用日志显示 SQL 执行成功返回 0 条但你去后端 MySQL 里查数据明明在。这种情况十有八九是路由问题。排查链路是这样的第一步去 MyCat 的日志里找这条 SQL 的路由记录。MyCat 的日志一般在logs/mycat.log里面会记录 SQL 被路由到了哪些 dataNode。如果日志显示路由到了dn_order_0但数据实际在dn_order_1那就是分片规则配错了。第二步检查rule.xml里的分片算法和schema.xml里的 dataNode 数量是否一致。比如count设成 2但 dataNode 只配了 1 个那余数 1 的数据就无处可去。第三步检查分片键的值。如果分片键是字符串取模算法会先转成哈希值再取模这个哈希值和 Java 的hashCode()可能不一样。MyCat 用的是自己的哈希算法所以同样的字符串在应用层算出来的分片和 MyCat 算出来的可能不同。这种情况建议在应用层用 MyCat 提供的分片算法库来预计算分片或者直接用数值型分片键。4.2 批量插入报“无法路由”错误这个错误信息通常是cant find datanode for sharding column。原因是批量插入的某一行数据其分片键值为 null 或者不在分片规则覆盖的范围内。比如按user_id取模分片但某一行的user_id是 nullMyCat 就无法计算路由。解决办法是在应用层做校验确保分片键不为空。或者在 MyCat 配置里设置默认分片节点让 null 值落到默认节点。table namet_order dataNodedn_order_0,dn_order_1 rulemod-long ruleRequiredtrue /ruleRequiredtrue表示必须匹配分片规则如果匹配不上就报错。设成 false 的话匹配不上会落到第一个 dataNode。我一般建议设成 true尽早暴露问题而不是让数据悄悄落到错误的分片。4.3 跨库 JOIN 导致的性能骤降单库时代JOIN 是数据库的强项。但在分库环境下如果两张表的分片规则不一致JOIN 就无法在单个分片内完成MyCat 需要把数据拉到中间层做归并 JOIN。这个代价非常大尤其是大表 JOIN。我遇到过一个案例订单表和用户表订单按user_id分片用户表按id分片。查询“某个用户的订单及用户信息”时MyCat 需要先从订单分片拿到订单再从用户分片拿到用户然后在中间层做 JOIN。数据量小的时候没问题数据量一大内存直接爆掉。解决办法有两个一是让关联表使用相同的分片规则这样 JOIN 可以在分片内完成。二是把 JOIN 拆成两次查询在应用层做组装。第二种方式虽然多了一次网络往返但内存可控而且可以利用缓存。// 先查订单 ListOrder orders orderMapper.selectByUserId(userId); // 再查用户 User user userMapper.selectById(userId); // 应用层组装 for (Order order : orders) { order.setUser(user); }这种方式在 MyBatis 里可以用association或者手动组装。手动组装更灵活也更容易控制查询次数。4.4 事务提交后数据不一致分库环境下的事务是个大话题。MyCat 支持弱分布式事务但默认不是强一致的。如果应用层用Transactional注解MyBatis 会把事务交给 MyCat 管理MyCat 再协调各个分片的事务。这个过程中如果某个分片提交失败其他分片可能已经提交了导致数据不一致。我一般建议如果业务能接受最终一致就用 MyCat 的弱事务配合补偿机制。如果必须强一致那就避免跨分片事务把相关数据设计在同一个分片内。比如订单和订单明细都用order_id分片这样它们永远在同一个分片事务就是本地事务。还有一个细节是 MyBatis 的SqlSession在 MyCat 下的行为。如果用了ExecutorType.BATCH批量操作的提交时机和单条操作不一样可能导致部分数据已经提交但部分还在缓冲区。这种情况建议在批量操作后显式调用flushStatements()确保所有语句都发送到 MyCat。5. 分片键选择与扩容预案的实战考量分片键选得好不好直接决定了分库方案能不能长期用下去。选得不好后期扩容就是噩梦。这一章聊聊分片键的选择逻辑和扩容预案。5.1 什么样的字段适合做分片键分片键的第一个要求是“高频查询条件”。如果大部分查询都带这个字段那分片键选它就对了。比如订单系统大部分查询都是“查某个用户的订单”那user_id就是天然的分片键。第二个要求是“分布均匀”。如果分片键的值分布不均匀比如按性别分片那数据就会倾斜。取模算法对分布均匀性要求很高哈希算法稍微好一点但也不能太偏。第三个要求是“不可变”。分片键一旦确定就不应该再修改。因为修改分片键意味着数据要从一个分片迁移到另一个分片这个代价很大。所以像status这种会变的字段不适合做分片键。第四个要求是“尽量是数值型”。数值型的分片键在取模和比较时性能更好而且不容易出现哈希不一致的问题。字符串分片键虽然也能用但要多一层哈希转换而且哈希算法在不同版本间可能有差异。5.2 取模分片的扩容困境与一致性哈希的取舍取模分片最大的问题是扩容。假设现在 2 个分片按user_id % 2分。要扩到 4 个分片变成user_id % 4那么原来余数 0 的数据里有一半要迁到新的分片 2原来余数 1 的数据里有一半要迁到新的分片 3。迁移量是总数据量的一半。一致性哈希可以解决这个问题。一致性哈希把分片节点映射到一个哈希环上数据按哈希值落到环上顺时针最近的节点。扩容时只影响相邻节点的数据迁移量是总数据量的 1/N。但一致性哈希的实现比取模复杂而且 MyCat 内置的一致性哈希算法在数据量小的时候可能分布不均匀。我的建议是如果数据量不大或者扩容频率很低用取模就行简单可靠。如果数据量很大而且预期会频繁扩容那就用一致性哈希或者用 MyCat 的range-long范围分片提前规划好分片范围。5.3 双写方案与数据迁移的过渡策略扩容的时候最稳妥的方案是双写。具体做法是新老两套分片规则同时运行应用层写数据的时候同时写老分片和新分片读数据的时候先读老分片如果读不到再读新分片。等数据迁移完成后再切换到只读新分片。这个方案的关键是“数据迁移”和“双写”的配合。数据迁移一般用工具做比如用 MyCat 的迁移工具或者自己写脚本。迁移过程中双写保证新数据不丢迁移完成后校验新老数据一致然后切换。双写的代价是写性能下降因为要写两次。而且双写期间的数据一致性需要额外保证比如用消息队列做异步补偿。如果业务能接受短暂的停机那停机迁移更简单但停机时间取决于数据量大表可能要好几个小时。我个人的经验是如果数据量在千万级以下停机迁移最省事。如果上亿那就老老实实做双写和渐进迁移。没有银弹只有取舍。6. 监控与日常运维中值得关注的指标分库方案上线之后运维的复杂度比单库高很多。这一章聊聊日常需要关注的指标和排查工具。6.1 MyCat 自身的关键指标MyCat 的管理端口 9066 提供了很多监控命令。常用的有show datanode查看各分片节点的状态show connection查看当前连接数show sql查看 SQL 执行统计。mysql -h127.0.0.1 -P9066 -uadmin -padmin show datanode; show connection; show sql;show datanode会显示每个 dataNode 的读写次数、活跃连接数、心跳状态。如果某个节点的活跃连接数一直很高说明这个分片可能有热点。show sql会显示 SQL 的执行次数、平均耗时、最大耗时可以用来发现慢查询。我一般会把这些指标接入监控系统设置告警阈值。比如某个分片的连接数超过 80% 就告警SQL 平均耗时超过 100ms 就告警。6.2 慢查询的定位与优化路径MyCat 的慢查询日志在logs/mycat.log里可以通过slowSqlTime参数设置阈值。定位到慢查询之后优化路径和单库类似加索引、改 SQL、调整分片规则。但分库环境下的索引有个特殊之处索引是建在每个分片上的不是全局的。所以如果查询不带分片键即使每个分片都有索引MyCat 还是要广播到所有分片总耗时是各分片耗时的叠加。这种情况下优化索引的效果有限关键还是要让查询带上分片键。还有一个容易忽略的点是 MyCat 的连接池。如果 MyCat 到后端 MySQL 的连接数不够SQL 就会排队等待。这个等待时间会计入 SQL 总耗时但看后端 MySQL 的慢查询日志却看不到。所以排查慢查询时要同时看 MyCat 的日志和后端 MySQL 的日志。6.3 数据倾斜的发现与再平衡数据倾斜是指某些分片的数据量远大于其他分片。取模算法在分片键分布均匀时不会倾斜但如果分片键本身分布不均比如按地区分片一线城市的用户量远大于三线城市就会倾斜。发现倾斜的方法很简单定期统计各分片的行数和磁盘占用。如果某个分片的行数是其他分片的几倍那就是倾斜了。再平衡的方法有两种一是调整分片规则把热点数据单独分片二是把热点数据迁移到独立的分片节点用更强的硬件。我遇到过一个案例按user_id取模分片但有一个大客户它的user_id恰好落在某个分片导致这个分片的数据量是其他分片的 10 倍。解决办法是把大客户的数据单独放到一个分片其他用户继续取模分片。这种“热点隔离”的思路在分库场景下很实用。7. 一些踩坑之后的个人体会整合 MyBatis 和 MyCat 做分库技术上的难点其实不多真正难的是“想清楚”。想清楚分片键选什么、想清楚哪些查询会广播、想清楚扩容怎么办、想清楚事务一致性要求到什么程度。这些问题在写代码之前就要有答案而不是等到上线之后才发现。我自己的习惯是在动手配置之前先画一张表列出所有核心业务查询标出每个查询的 WHERE 条件、排序字段、返回行数。然后根据这张表来决定分片键和分片规则。这张表后来也成了联调时的检查清单每跑通一个查询就打个勾。还有一个体会是不要迷信“一步到位”。分库方案可以分阶段做先做读写分离再做分库最后做分表。每个阶段解决一个问题风险可控。一次性把所有东西都上了出了问题很难定位是哪个环节的错。最后说一个具体的技巧在 MyBatis 的 Mapper 里给所有分片键查询加一个命名规范比如方法名以ByShardingKey结尾。这样在代码审查的时候一眼就能看出哪些查询是走分片的哪些是广播的。广播查询要特别标注说明为什么不能带分片键以及预期的性能代价。这个习惯坚持下来能避免很多无意识的广播查询。