1. 先搞清楚这个压缩包里装的是什么
MyCat 是一个用 Java 写的数据库中间件,它对外伪装成一台 MySQL 服务器,对内把一条 SQL 拆分、路由、改写,分发到后面若干台真实的 MySQL 实例上去执行,再把结果集合并回来。你拿到的这个Mycat-server-1.6.7.6-release-windows-linux.rar,本质上就是一份绿色免安装的运行目录:解压、改配置、跑启动脚本,三个动作就能让一台"逻辑上的大数据库"跑起来,而底层其实是多个物理库在干活。它主要解决三类问题——单表数据量涨到几千万行后索引撑不住、单库连接数和 IO 顶到天花板、以及业务上天然可以水平切分的场景(订单、日志、账单这类按用户或按时间天然隔离的数据)。适合谁来读?如果你手上有 MySQL 分库分表的诉求,又不想在应用层硬编码路由逻辑,那这份东西值得花半天时间摸一遍;如果你只是想搭个单机数据库,那真的没必要上中间件,多一层就多一个故障点。
很多人对这个包的困惑集中在两件事:一是版本号后面那一串数字到底代表什么,二是 windows 和 linux 打在一个包里是什么意思。先回答第二个,windows-linux只是说明压缩包里的启动脚本两套都给齐了——bin目录下既有.bat也有.sh,核心的jar包和配置文件是同一份,跟平台无关,你按操作系统挑脚本用就行。第一个问题稍微复杂一点,得从 MyCat 的版本分支说起,下面的内容会一点点拆开。
我自己第一次碰这个东西是在一个订单库撑不住的场景里,当时评估过应用层 sharding、代理层中间件、原生分区表三条路,最后选了 MyCat,原因很朴素:改动面最小,应用侧的 JDBC 连接串换个端口就行,代码几乎不用动。这个选择的代价和后续踩的坑,后面几节会挨个讲清楚。
2. 1.6.7.6 这个版本号背后的选择逻辑与包内容
2.1 从版本号读出维护状态与兼容边界
MyCat 的版本号规则大致是"大版本.分支.修订.补丁"这样的四段式,1.6.7.6 落在 1.6 这个分支的后期维护带上。1.6 系列是这条产品线里被验证得最久、资料最多的一支,网上你能搜到的绝大多数配置示例、分片算法说明、问题帖,都是围绕 1.6 展开的。这带来的直接好处是:你遇到的坑,大概率已经有人踩过了;坏处是,一些早期设计上的妥协被保留了下来,比如跨库 join 的支持一直很有限,分布式事务走的是弱 XA 路线而不是强一致的两阶段提交。
至于为什么要在 1.6 的众多小版本里挑 1.6.7.6,我的判断依据主要有三条。第一,看它是不是该分支的末期版本,越靠后的补丁版本通常把前面暴露的连接泄漏、结果集合并越界、管理端口命令失效这类问题修得更干净。第二,看你后端 MySQL 的版本,1.6 系列内置的 JDBC 驱动偏老,如果你的后端是较新版本的 MySQL,驱动大概率要手工替换,这一点在选版本时就要提前想好。第三,看社区活跃度,一个版本如果长期没有新 issue 也没有回复,说明维护基本停了,那就只能当"能跑就别动"的稳定件来用,别指望出问题有人接。
注意:不要迷信"版本越新越好"。数据库中间件这类组件,稳定性和可预测性远比新特性重要,一个跑了两年没出过事的旧版本,比一个刚发出来三个月的新版本更值得留在生产上。
2.2 压缩包里到底有哪些东西值得看
解压之后你会看到一个标准目录结构:bin放启动停止脚本和 wrapper 相关文件,conf放全部配置文件,lib放依赖 jar,logs是日志输出目录,catlet放一些扩展用的脚本类东西,根目录下通常还有个version.txt之类的说明文件。真正需要你动手的只有conf和bin两个目录,其余的基本不用碰。
conf里的文件值得挨个点名。server.xml管用户、逻辑库授权和系统级参数;schema.xml是核心中的核心,定义逻辑库、逻辑表、数据节点、物理数据源的映射关系;rule.xml定义分片规则和算法实现;sequence_conf.properties配合全局序列使用;wrapper.conf控制 JVM 启动参数;log4j2.xml管日志级别和滚动策略。剩下还有几个dnindex.properties之类的状态文件,是运行期自动维护的,别手动改。
bin目录在 Windows 下有startup.bat、startup_nowrap.bat、mycat.bat,在 Linux 下有startup.sh、mycat。这里的差别值得单独说一下:带wrapper的启动方式会在前台起一个守护进程再拉起 JVM,好处是能做自动重启,坏处是 Windows 下会多弹一个控制台窗口,容器化或者用进程管理工具托管的时候容易出乱子;nowrap版本直接java -jar拉起,简单粗暴,适合调试和受控环境。
2.3 获取渠道与完整性校验这件事
这类中间件的分发包,来源基本就三类:项目官方的发布页面、开源代码托管平台的 release 附件、以及各类镜像站点和二次打包的整合包。前两类拿到的是原始产物,第三类就要小心了——我见过不少所谓"某某优化版""某某增强版",里面被塞了改过的 jar 包,类名一样、行为不一样,排查起来极其痛苦,因为你看源码对不上。
所以我的习惯是:优先从官方发布渠道拿原始包,拿到之后立刻核对校验值。正规的发布页一般会同时提供.md5或.sha1校验文件,Windows 下用certutil -hashfile 文件名 MD5,Linux 下用md5sum 文件名,两边算出来的值对不上就别解压了,直接重新下。这一步很多人嫌麻烦跳过,但真出事的时候,它是唯一能证明你手上这份东西没被动过手脚的依据。
另外提醒一句,.rar格式在 Linux 下不一定有现成的解压工具,unrar往往需要单独装,实在不行就在 Windows 或本地机器上解压好再传上去。解压路径不要带中文、不要带空格,这是老生常谈但每年都有人栽在这上面——wrapper 脚本里对路径的处理没你想的那么健壮。
3. 它到底解决了什么问题:映射链条与适用边界
3.1 一条 SQL 从客户端到物理库的完整旅程
理解 MyCat 最快的方式,是把它的处理链路走一遍。客户端连上 8066 端口,MyCat 先用 MySQL 协议完成握手和认证,此时它在你眼里就是一台普通的 MySQL。你发过来一条select * from t_order where order_id = 1001,MyCat 拿到之后做几件事:解析 SQL 语法树,识别出表名t_order;去schema.xml里查这张表绑定了哪些数据节点;拿到order_id = 1001这个条件,套用rule.xml里配置的分片算法算出落在哪个节点上;然后把原始 SQL 稍作改写(主要是补上库名前缀或者调整分页参数),通过后端连接池发到对应的物理 MySQL 上。
结果回来之后,如果只命中一个节点,基本就是原样透传;如果命中了多个节点,麻烦就来了——需要做结果集合并。合并包括几件事:字段对齐、行合并、排序归并、分页裁剪、聚合计算。order by的实现是每个分片各自排序后做多路归并;limit 100, 10会被改写成每个分片取limit 0, 110,然后在内存里跳过前 100 条;count(*)是把各分片的计数加起来;avg()更绕一点,要拆成sum和count分别取回再相除。这些细节决定了你写 SQL 时的注意事项,后面第 5 节会展开。这个链路听着不复杂,但每一步都可能成为性能瓶颈或者结果错误的来源。
3.2 分片规则:把一行数据送到它该去的节点
分片算法的选择,是整个方案里最需要前置思考的部分,因为它一旦定下来,后期改动的成本极高——改分片规则意味着全量数据重新分布。MyCat 提供了十几种现成的算法实现,常用的可以归成四类:取模类、范围类、哈希类、日期类。
取模类最直观,order_id % 节点数,数据分布均匀,缺点是扩容时要重新洗数据,而且取模的字段必须是稳定的、不会变的。范围类按区间划分,比如 0 到 500 万走节点一,500 万到 1000 万走节点二,好处是范围查询能精准命中单个节点,坏处是数据量增长不均时会热点集中,而且需要提前规划好区间上限。哈希类用一致性哈希或者 MurmurHash,扩容时只影响相邻节点的数据迁移,代价是要额外维护虚拟节点。日期类按月或按天分片,日志、账单这类带时间属性的表特别合适,查询条件里带时间范围时能有效裁剪。
选哪一类,取决于你的查询模式。如果你的查询几乎都会带用户 ID,那按用户 ID 取模是最省事的;如果你的查询大量是"最近七天的订单"这种时间范围,那按日期分片能把扫描范围压到最小。最怕的是那种"什么条件都可能带"的查询,这种表要么做成全局表,要么就得接受每次查询广播到所有分片的代价。
3.3 哪些场景我建议你绕开它
不是所有分库分表的需求都该用中间件解决,这一点得说清楚。如果你的分片逻辑非常固定,永远是按用户 ID 切、而且切分规则三年内不会变,那在应用层用个简单的路由组件做掉,链路更短、排障更直接,中间件这层的价值就不大了。如果你的业务强依赖分布式事务的强一致性,比如金融级的转账场景,那 MyCat 的弱 XA 大概率满足不了你,得上专门的事务协调组件。如果你的数据量其实没到瓶颈,只是查询慢,那该做的是加索引、优化 SQL、上缓存,而不是引入分库分表,因为后者带来的复杂度是成倍增加的。
还有一个容易被忽略的点:中间件会把很多数据库层面的报错"模糊化"。原生 MySQL 报错会告诉你具体哪个字段、哪一行、什么约束冲突;经过 MyCat 之后,有些错误会被包装成通用的执行异常,定位成本明显上升。所以上中间件之前,先想清楚团队有没有能力承接这种排障成本的上升。
4. 落地第一步:环境准备与启动验证
4.1 JDK 与后端数据库的前置检查
MyCat 是纯 Java 应用,JDK 是硬依赖。1.6 系列的编译目标比较老,用 JDK 8 跑是最稳的组合,JDK 11 及以上不是完全跑不起来,但可能遇到反射相关的报错,尤其是老版本的日志框架和字节码库在模块化限制下容易出问题。所以我的建议很明确:单独装一个 JDK 8 给 MyCat 用,别跟其他服务混用。
后端 MySQL 这边,需要提前准备好若干个真实的库,库名、字符集、账号权限都要配好。字符集建议统一成utf8mb4,不然中文和表情符号过一遍中间件之后可能出现乱码,而且这种乱码往往在测试阶段发现不了,上了生产才暴露。账号权限方面,MyCat 连接后端的账号需要具备建表之外的常规读写权限,如果开了自动建表或者全局序列表,还要加上对应的 DDL 权限。还有一点:后端库的表结构必须完全一致,索引也要一致,否则路由到不同分片的同一条 SQL 可能表现不同,这类问题极其难查。
4.2 解压后的目录结构与关键文件
解压到一个干净的路径,比如D:\middleware\mycat或者/opt/mycat。目录结构长这样:
mycat/ ├── bin/ # 启动停止脚本 ├── catlet/ # 扩展脚本 ├── conf/ # 全部配置文件 ├── lib/ # 依赖 jar ├── logs/ # 日志输出 └── version.txtconf里最先要动的是三个 XML。动手之前先做一件事:把原始的conf目录整体复制一份备份,命名成conf.bak。我见过太多人改崩了配置之后想回滚,结果发现原文件被覆盖了,只能重新解压。这一步花不了三十秒,救命的概率却不低。
lib目录里有个细节值得留意:里面内置的mysql-connector-java版本通常偏老。如果你后端是较新的 MySQL,驱动需要手工替换——把新版本的驱动 jar 放进去,老版本的删掉或者改名备份。替换的时候注意别同时留两个版本的驱动,类加载顺序不确定,容易出现莫名其妙的连接异常。
4.3 Windows 与 Linux 的两种启动方式
Windows 下,双击startup.bat是最直接的方式,它会弹出两个控制台窗口,一个 wrapper 守护、一个 JVM 主进程。调试阶段我更推荐用startup_nowrap.bat,只有一个窗口,日志直接打屏,Ctrl+C 就能停,看启动异常特别方便。mycat.bat start是命令行的等价写法,适合写进批处理脚本里。
Linux 下的用法是bin/mycat start、bin/mycat status、bin/mycat stop三件套,底层还是调startup.sh。这里有两个常见坑:一是脚本要有执行权限,chmod +x bin/*.sh先做一遍;二是脚本里对JAVA_HOME的依赖,如果系统里没配环境变量,需要在conf/wrapper.conf里显式指定wrapper.java.command指向 java 可执行文件的绝对路径。另外,logs/wrapper.log是启动阶段的第一手信息源,如果进程起不来、控制台什么也没打印,先去翻这个文件,八成能直接看到原因。
启动参数也要提前调。默认的 JVM 堆内存在中小数据量下够用,但如果你的分片多、并发高、结果集合并频繁,堆会很快吃满,表现是频繁 Full GC 甚至 OOM。我一般会显式设置成-Xms2g -Xmx2g起步,然后观察logs目录下的 GC 情况再决定要不要往上加。直接在wrapper.conf里改wrapper.java.additional那几行就行。
4.4 用 9066 和 8066 两个端口做启动验证
启动成功的标志不是"窗口没关",而是要验证两个端口都通了。8066 是数据端口,对外提供 MySQL 协议服务;9066 是管理端口,用来查看运行状态、执行运维命令。这两个端口的定位完全不一样,很多人只知道 8066,遇到问题就抓瞎。
验证方式很简单,用任何 MySQL 客户端连 9066,执行show @@help能看到一屏命令列表就说明起来了。常用的几条:show @@database看逻辑库列表,show @@datanode看数据节点状态,show @@datasource看后端连接池,show @@heartbeat看后端库的心跳检测结果。心跳那一条尤其重要,它会告诉你每个物理库是OK还是ERROR,连不上后端的时候这里一眼就能看出来。
再连 8066,用server.xml里配置的账号登录,执行show databases应该能看到你定义的逻辑库名。到这一步,环境就算通了。接下来才是真正花时间的部分——把schema.xml和rule.xml配对写好。
提示:管理端口 9066 只应该在受控网络内开放,不要暴露到公网。它没有额外的权限校验层,能连上就能查配置、看连接、执行运维命令。
5. 三份核心配置文件的逐项拆解
5.1 server.xml:用户、逻辑库与系统参数
server.xml的结构很简单,一个system节点管全局参数,下面若干个user节点管账号。最常改的几个系统参数:sequnceHandlerType决定全局序列的生成方式(0 是本地文件、1 是数据库、2 是时间戳算法、3 和 4 是基于协调服务的分布式 ID),做分片表并且需要自增主键的话,这个必须配,否则插入时主键会冲突;defaultSqlParser指定 SQL 解析器,1.6 默认用 druid 解析器,兼容性最好;useOffHeapForMerge控制结果集合并是否走堆外内存,数据量大的时候打开能减少 GC 压力。
用户配置是这样的:
<user name="app_user" defaultAccount="true"> <property name="password">your_password</property> <property name="schemas">ORDER_DB</property> <property name="readOnly">false</property> </user>schemas是授权给这个用户的逻辑库列表,多个用逗号分隔。这里有个隐含的坑:逻辑库名必须和schema.xml里schema节点的name属性严格对应,大小写敏感,写错了不会报配置错误,但登录后show databases是空的,会让人以为服务没起来。readOnly设成true时,这个账号执行写操作会被拒绝,适合做只读报表账号。
5.2 schema.xml:分片表、全局表与数据节点
这份文件是整套配置的骨架,三层结构从外到内:schema定义逻辑库,dataNode定义数据节点(逻辑库里的一个分片),dataHost定义物理数据源。中间用table节点把逻辑表和分片规则绑起来。
<schema name="ORDER_DB" checkSQLschema="false" sqlMaxLimit="100"> <table name="t_order" dataNode="dn1,dn2,dn3,dn4" rule="mod-long" primaryKey="order_id"/> <table name="t_user" dataNode="dn1" primaryKey="user_id"/> <table name="t_dict" dataNode="dn1,dn2,dn3,dn4" type="global" primaryKey="id"/> </schema> <dataNode name="dn1" dataHost="host1" database="order_db_0"/> <dataNode name="dn2" dataHost="host1" database="order_db_1"/> <dataNode name="dn3" dataHost="host2" database="order_db_0"/> <dataNode name="dn4" dataHost="host2" database="order_db_1"/> <dataHost name="host1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100"> <heartbeat>select user()</heartbeat> <writeHost host="hostM1" url="127.0.0.1:3306" user="root" password="pwd"/> </dataHost>几个参数值得单独解释。sqlMaxLimit是个保护阀,客户端不带limit的查询会被自动加上这个上限,防止一次拉回几百万行把内存打爆,测试阶段可以调小到 10 方便观察,生产上按业务定,通常 100 到 1000 之间。checkSQLschema控制是否剥离 SQL 里的库名前缀,如果你的应用习惯写ORDER_DB.t_order,设成true更友好;如果你的代码里混用了多种写法,设false然后统一规范。
balance控制读操作的分配策略,0 是不分离、1 是所有从库参与读负载、2 是读写都随机分发、3 是只从从库读。writeType控制写操作的路由,0 是只发第一个 writeHost,1 是按双主随机发。switchType是主从切换策略,1 是默认的自动切换。dbDriver我建议用native,它走的是 MyCat 自己实现的后端连接,比jdbc模式性能好一些,代价是只能在 MySQL 协议下用。
type="global"的全局表是个很实用的设计:字典表、配置表这类数据量小、几乎不变的表,在每个节点上都存一份完整副本,跨节点 join 的时候只在本地做,不需要数据搬运。代价是写操作要同步到所有节点,所以只适合读多写极少的数据。
5.3 rule.xml:分片算法的选择与参数
rule.xml里分成两段,tableRule定义"哪张表的哪个字段用什么算法",function定义"这个算法具体怎么算"。
<tableRule name="mod-long"> <rule> <columns>order_id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule> <function name="mod-long" class="io.mycat.route.function.PartitionByMod"> <property name="count">4</property> </function>columns是分片字段,count是分片数量。这里的count必须和schema.xml里dataNode列出的节点数严格一致,写 4 但只配了 2 个节点,落不到的节点上会直接报错。这是最常见的配置错误之一,改分片的时候两边一定要同步改。
如果要按字符串哈希分片,用PartitionByHash,它有个hashSlice参数控制对字符串的哪一段做哈希,格式是start:end的切片写法,比如0:3表示取前三个字符,-4:0表示取后四个字符。这个参数调得好不好,直接影响数据分布是否均匀。我见过用手机号做分片字段但只取前 3 位的配置,结果所有 138 开头的号码全挤在一个分片上,等于没分。
按日期分片用PartitionByDate,需要配dateFormat、beginDate、partionDay三个参数。beginDate是起始日期,partionDay是每个分片覆盖的天数。这里最坑的地方是:一旦数据写入,beginDate就绝对不能改,改了会导致旧数据算出来的分片位置对不上,查询直接查不到。所以配这个参数的时候,起始日期要往前留足余量,宁可多分几个空片,也别事后想改。
5.4 一个两库四表的分片实例与参数计算
光看配置没感觉,走一遍完整的例子。假设场景是订单表,规划两个物理库order_db_0和order_db_1,每个库里建两张表t_order_0和t_order_1,总共四个数据节点,按order_id取模分片。
先建库建表,两个库结构完全一致:
CREATE DATABASE order_db_0 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE order_db_1 DEFAULT CHARACTER SET utf8mb4; CREATE TABLE order_db_0.t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(12,2) NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (order_id), KEY idx_user (user_id) ) ENGINE=InnoDB;其余三张表照抄,只改库名和表名。注意这里的主键是order_id,没有用自增,因为分片环境下自增主键会冲突,要么用全局序列,要么用业务侧生成的雪花 ID。
接着配dataNode,四个节点分别指向两个库里的两张表:
<dataNode name="dn1" dataHost="host1" database="order_db_0" table="t_order_0"/>这里有个细节,dataNode的database属性可以写成order_db_0.t_order_0的合体形式,也可以用单独的table属性,两种写法等价,选一种保持统一就行。
然后是rule.xml里配mod-long,count设成 4。算法很简单:order_id % 4的结果决定路由。我们来算几条:
| order_id | order_id % 4 | 目标节点 | 物理位置 |
|---|---|---|---|
| 1001 | 1 | dn2 | order_db_0.t_order_1 |
| 1002 | 2 | dn3 | order_db_1.t_order_0 |
| 1003 | 3 | dn4 | order_db_1.t_order_1 |
| 1004 | 0 | dn1 | order_db_0.t_order_0 |
注意节点编号和取模结果差一位,dn1对应余数 0。这个对应关系是由schema.xml里dataNode属性的书写顺序决定的,写dataNode="dn1,dn2,dn3,dn4"就意味着余数 0 落dn1、余数 1 落dn2,以此类推。这个顺序一旦上线就不能随便调整,调整等于全量数据重分布。
最后验证。连 8066,往t_order插四条数据,然后分别去四个物理表里select count(*),每个表应该正好一条。再执行select * from t_order where order_id = 1002,看是不是只返回一条,同时去 9066 执行show @@sql之类的命令看执行记录,确认只打到了一个节点上。这一步验证通过,分片就算真正跑通了。
6. 实测中踩过的坑与排查路径
6.1 启动阶段起不来,先看这三处
启动失败的表现形式很多,但排查顺序基本固定。第一步看logs/wrapper.log,如果连 JVM 都没拉起来,这个文件里会有明确的原因,最常见的是JAVA_HOME没配或者路径里有空格。第二步看logs/mycat.log,JVM 起来了但应用初始化失败,日志会打在这里,配置文件的 XML 语法错误、端口被占用、依赖 jar 缺失都会在这一层暴露。第三步才是看控制台输出,因为前面两个文件通常已经把话说清楚了。
XML 语法错误是高频问题,尤其是手改配置的时候多删了个标签闭合。有个快速自检的办法:用任何支持 XML 格式化的编辑器打开,格式化一遍,如果结构乱了,说明括号不配对。另外注意server.xml和schema.xml里的注释不能嵌套,<!-- <!-- --> -->这种写法会直接解析失败。
端口占用也遇到过几次,表现是启动日志里刷一串Address already in use,但进程看着还在。这时候先netstat -ano | findstr 8066(Windows)或者lsof -i:8066(Linux)确认谁占着,别急着重启,先把占用进程处理掉。
6.2 客户端连不上或者连上就断
能 ping 通端口但客户端连不上,问题通常出在认证环节。最典型的是用较新版本的 MySQL 客户端连接时,客户端默认走的是较新的认证插件,而 MyCat 只支持传统的密码认证方式,握手阶段直接失败,报错信息还相当含糊。解决办法是在连接串里显式指定认证插件,或者在服务端把账号的认证方式改掉。这个坑在升级客户端版本之后特别容易撞上,因为老客户端能连、新客户端不能连,很容易误判成服务端坏了。
另一类是连上之后过一会儿自动断开。检查dataHost里的maxCon和minCon,minCon是连接池的保活下限,设得太小会导致连接频繁创建销毁,加大后端压力;maxCon设得太大则可能把后端 MySQL 的连接数打满。一般minCon给 10,maxCon按单机 500 到 1000 起步,然后观察后端show processlist的实际占用。
还有一种情况是连上了、查询也返回了,但客户端会报一些协议层面的警告。这类问题通常和字符集有关,检查连接串里的characterEncoding和后端库的字符集是否一致,不一致的时候中文数据会出现截断。
6.3 路由结果和预期对不上
明明order_id = 1002应该落在dn3,结果查不到数据,或者查出来是空的。排查的第一步是在 9066 端口执行show @@datanode,看四个节点的状态是不是都是OK,有ERROR的先解决连通性。第二步是确认数据到底写进了哪个物理表,用后端账号直连四个表挨个select count(*),如果数据实际落在别的表里,说明分片字段的值和你以为的不一样——比如order_id在应用侧是字符串"1002",取模前做了类型转换,结果可能和你手算的不一致。
还有一种更隐蔽的情况:分片字段在 SQL 里被函数包裹了。where abs(order_id) = 1002这种写法,解析器识别不出分片字段的原始形态,只能退化成全节点广播,结果虽然对,但性能损失巨大,从 9066 的show @@sql里能明显看到一条 SQL 被打到了四个节点。这类写法要在代码评审阶段就拦住,分片字段上不要套任何函数。
6.4 跨库查询和结果合并的陷阱
跨分片的order by加limit,是最容易出问题的组合。上面说过,limit m, n会被改写成每个分片取m + n条再内存裁剪,如果m特别大,比如翻到第 100 万条,那每个分片都要取回 100 万条,内存直接崩。大偏移量的深分页,在分片环境下几乎必然出问题,正确的做法是改成基于游标的翻页,用上一页最后一条的 ID 作为下一页的起始条件,把offset消掉。
count(distinct xxx)也要小心。如果去重的字段不是分片字段,每个分片算出来的去重结果可能包含重复值,MyCat 需要在内存里再做一次去重,数据量大时同样是内存风险。group by也是类似道理,分组键不是分片键时,必须把所有分片的数据拉回来统一分组。
跨库 join 干脆就别指望。1.6 只支持基于父子表关系的有限 join,需要你在schema.xml里用childTable显式声明关联关系,而且关联条件必须严格匹配。任意表之间的 join 会退化成广播,性能惨不忍睹。这种查询老老实实拆成两次单表查询,在应用层做组装,比自己跟中间件较劲省事得多。
6.5 常见问题速查表
| 现象 | 大概率原因 | 排查动作 |
|---|---|---|
| 启动无输出,进程消失 | JAVA_HOME 未配或路径含空格 | 查 wrapper.log,检查环境变量 |
| 8066 连不上,9066 正常 | server.xml 用户或 schemas 配错 | 核对逻辑库名大小写 |
| 客户端握手失败 | 认证插件不兼容 | 连接串显式指定传统认证方式 |
| 查询返回空但数据存在 | 分片字段被函数包裹导致广播失败 | 直连物理库核实数据落点 |
| 跨分片分页越翻越慢 | 深分页改写放大 | 改为游标翻页 |
| 主键冲突 | 未配置全局序列 | 检查 sequnceHandlerType 与序列表 |
| 主从切换后写入失败 | switchType 与主从状态不匹配 | 查 show @@heartbeat 与后端主从状态 |
| 内存持续上涨 | 结果集合并占用堆内内存 | 开堆外合并,或缩小 sqlMaxLimit |
7. 上线之前值得想清楚的几件事
配置跑通和能在生产上跑,中间还差着一段距离。我自己的经验是,这套东西的复杂度大头不在启动,而在"出事的时候你能不能快速定位"。所以上线前有件事必须做:把 9066 端口的常用命令整理成一页纸贴在团队文档里,尤其是show @@sql、show @@connection、show @@heartbeat、reload @@config这几条,出问题的时候能省下大量时间。reload @@config特别值得记住,改完schema.xml和rule.xml之后不用重启进程就能生效,但server.xml的改动一般还是要重启,这一点别搞混。
扩容这件事要提前想。取模分片从 4 个节点扩到 8 个,所有数据都要重新分布,这个迁移量在大表上可能是几十小时。如果预判到数据会持续快速增长,一开始就该选一致性哈希或者预留足够的范围分片,哪怕前期节点数少、有些节点是空的,也比后期停机迁移划算。这个判断没有标准答案,取决于你的数据增长曲线和可接受的停机窗口。
最后一点,也是我个人体会最深的:中间件这层一定要有降级方案。当 MyCat 出问题的时候,你的应用能不能快速切回直连某个物理库?哪怕只能保证部分核心功能可用,也比全站不可用强。我见过没有降级预案的团队,中间件一挂,排查和恢复的时间全部变成了故障时长。提前在配置里留好直连的开关,把切换步骤写进预案并演练一遍,这件事的价值在你真正用上它的那天才会体现出来。