Mycat2分库分表实战:启动链路、三层配置与分片表避坑
简介:本资源为 Mycat2 分布式数据库中间件的基础安装包,面向需要在本地或服务器上搭建数据库分片与读写分离架构的 Java 开发者和运维人员,用于解决单机数据库在高并发、大数据量场景下的扩展性瓶颈。压缩包共 51 个文件,约 1.2MB,以 sql 初始化脚本、so/jnilib/a/dll 等多平台本地库、json 与 xml 配置、bat/mycat 启动脚本及 jar 依赖为主,覆盖 Windows、Linux、macOS、AIX、Solaris 等系统的 wrapper 适配文件,解压后按需修改 schemas、datasources、server 等配置即可启动服务。已有 1072 人学习下载,可作为理解数据分片规则、SQL 路由、读写分离与动态扩缩容的实践起点,包内附带的示例 SQL 与多平台启动脚本也便于对照排查环境问题、快速验证部署流程。
1. 拿到 mycat2 基础安装包先别急着 start
单体 MySQL 撑到某个量级,最先出问题的往往不是 SQL 写得烂,而是单表几千万行之后加个索引要锁半小时、备份窗口越来越长、主从延迟一抖业务就报警。这时候团队通常会想到分库分表中间件,mycat2 基础安装包就是干这个的:它是一个说 MySQL 协议的代理层,应用连它和连 MySQL 几乎没有区别,分片路由、读写分离、全局表这些逻辑都在代理这一层做掉,业务侧的 JDBC 连接串改个端口就能接上去。
它适合谁?手上已经有一套能跑的 MySQL、又不愿意为了分库分表去改业务代码里那堆 SQL 和连接的团队。不适合谁?指望着装完就自动分片的人——mycat2 的元数据是要你自己一条条配出来的,数据源、集群、逻辑库、分片表,四层东西少一层就跑不通。下面按我实际拆包的顺序写:先看包结构和启动链路,再讲三层配置怎么串,然后是分片表落地和那五个必踩的坑。
2. 解压到第一次连上 8066:包结构与启动链路
拿到包之后第一件事不是敲 start,而是把目录看一遍。mycat2 的运行态很简单:一个 Java 进程,读 conf 下的元数据,对外开一个 MySQL 协议端口,把 SQL 转发到后面真实的 MySQL 实例。所以它出问题的地方也就那么几处——JDK 版本、conf 里的 JSON 格式、端口占用、以及后面物理库连不连得上。把这几处的检查顺序固定下来,后面 debug 能省掉一大半时间。
2.1 四个目录各管什么
解压后基本就是 bin、conf、lib、logs 四个目录,其中 conf 是你以后动得最多的地方,lib 基本不动,logs 是每次启动失败的第一站。
| 目录/文件 | 作用 | 什么时候动它 |
|---|---|---|
| bin/ | 启停脚本,Linux 下是 mycat,Windows 下是 mycat.bat | 调 JVM 堆大小、换启动用户 |
| conf/server.yaml | 服务端口、线程数这类进程级参数 | 改端口、压测时调线程池 |
| conf/datasources/ | 每个物理 MySQL 一个 json | 加实例、改连接池大小 |
| conf/clusters/ | 主从组,写节点和读节点列表 | 加从库、调读策略 |
| conf/schemas/ | 逻辑库和逻辑表元数据 | 加库、加分片表 |
| conf/users/ | 连接代理用的账号 | 上线前必须改默认密码 |
| lib/ | 依赖 jar 和自带的算法类 | 换 MySQL 驱动、扩分片算法 |
| logs/ | mycat.log 及滚动历史 | 启动失败、路由异常 |
一个容易忽略的点:conf 下的这些 json 不是"参考配置",是运行时的唯一真相。你在客户端里用命令动态建出来的数据源、集群、逻辑库,最终也是落到这几个目录里。所以千万别一边手改文件、一边用命令建同名对象,两边打架的时候 reload 会给你一个很难看懂的错误。
2.2 JDK、启动脚本与端口
mycat2 是 Java 进程,JDK 版本不匹配的典型表现是进程起来几秒就没了,日志里只有一行看不懂的异常。所以第一步永远是确认 java 版本,再解压。
# 1. 确认 JDK:8 或 11 是主流验证过的组合,别在第一次跑通前折腾太新的版本 java -version # 2. 解压到固定目录,路径里不要带空格和中文 mkdir -p /opt/mycat2 unzip mycat2-basic.zip -d /opt/mycat2 cd /opt/mycat2 # 3. 先看启动脚本里最终拼出来的 java 命令,堆大小和 classpath 都在这 sed -n '1,80p' bin/mycat # 4. 启动并查状态 bin/mycat start bin/mycat status # 5. 确认端口真的监听了,status 说 RUNNING 不代表端口起来了 ss -lntp | grep 8066这段里第 3 步是关键。启动脚本本质上就是拼一条java -cp ... 主类的命令行,堆大小、GC 参数、配置文件路径都写在里面。第一次跑不通的时候,把这条命令手工敲一遍(去掉后台重定向),异常会直接打在终端上,比翻日志快得多。堆大小我一般从 2g 起步,代理层本身不存数据,内存主要花在连接缓冲和 SQL 解析上,给太大反而拖长 GC 停顿。
参数上要盯住两个:一是-Xmx,二是 server.yaml 里的监听地址和端口。默认端口是 8066,如果这台机器上已经有别的实例,改 server.yaml 比改脚本靠谱。启动脚本报"端口已被占用"的时候,用ss -lntp | grep 8066找到占用进程,别急着 kill,先确认是不是上一次没停干净的自己。
启动失败时不要从头读日志,几万行读不完:
# 只看最后 80 行,异常栈的根因通常在最后 tail -n 80 logs/mycat.log # 直接把错误行捞出来,Caused by 才是真正的根因 grep -nE "ERROR|Exception|Caused by" logs/mycat.log | tail -n 30如果 logs 下的 mycat.log 是空的,去看同目录下启动脚本重定向产生的那个控制台日志。这种情况一般是 JVM 在加载类阶段就挂了,业务日志框架还没来得及初始化,所以业务日志里什么都看不到——这也是为什么第 3 步手工执行那么重要。
2.3 用 mysql 客户端验证代理真的活了
进程起来、端口监听,只能说明它活着,不能说明元数据是好的。真正的验收标准是:能用 MySQL 客户端连上,并且能看到数据源列表。
# 客户端版本别太老,太老的可能在握手阶段就断连 mysql -h127.0.0.1 -P8066 -uroot -p123456-- 先看库列表,确认连的是代理而不是背后某个物理库 show databases; -- 再看数据源:这是管理命令,写在 /*+ */ 注释里,不是普通 SQL /*+ mycat:showDataSources{} */; /*+ mycat:showClusters{} */; /*+ mycat:showSchemas{} */;这三条命令返回的是代理内存里的元数据快照,能正常返回就说明配置文件解析没问题、内置的 prototype 数据源至少加载成功了。如果show databases只有一两个库、和你预期的对不上,问题一定在 conf/schemas 下,不在网络。
参数上注意两点:默认账号密码在 conf/users 下的 user json 里,明文写着,上线前必须改掉,并且要把这个账号能访问的逻辑库列表一起收窄;端口 8066 是服务端口,管理命令走的是同一个连接,不需要另开管理端口,这一点和很多老版本中间件不一样,别去找 9066。
3. 三层配置模型:数据源、集群、逻辑库怎么串起来
mycat2 的配置之所以让新手懵,是因为它把"一台物理库"和"一个逻辑库"彻底拆开了。你要先在数据源层描述清楚每台真实的 MySQL 在哪、账号是什么、连接池多大;再在集群层把若干数据源组成一个主从组,说明谁是写、谁是读;最后在逻辑库层给应用一个它认识的库名,并挂上这个库的表元数据。三层缺一层,SQL 就没有完整的路由路径。
3.1 一条 SQL 到物理库要过几道关
应用连上来的时候带的是逻辑库名和用户名,代理先按 users 里的配置校验账号、确认这个账号能访问哪些逻辑库;然后按逻辑库找到它绑定的集群;再按集群找到写节点和读节点列表;最后根据 SQL 是读还是写、表是不是分片表,决定发到哪台物理库的哪个库名上。这条链路里任何一环名字对不上,报错都长得很像——基本就是Unknown database或者连接被拒。
所以配的时候有个笨办法特别有效:三个名字(数据源名、集群名、逻辑库名)提前写在纸上,全程只用这一套命名,别一会儿 dw0 一会儿 ds0。我见过不止一次因为手抖把 replicas 里写了个不存在的数据源名,折腾半天以为是网络问题。
3.2 数据源:一个 json 就是一台物理 MySQL
数据源配置放在 conf/datasources 下,一个文件一个实例。下面是骨架,字段名以你包里自带的样例文件为准,不同小版本可能有增删。
{ "name": "dw0", "type": "JDBC", "dbType": "mysql", "url": "jdbc:mysql://127.0.0.1:3306/db1?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai", "user": "root", "password": "123456", "minCon": 2, "maxCon": 50, "maxConnectTimeout": 3000, "maxRetryCount": 3, "instanceType": "READ_WRITE", "weight": 0 }这个文件里真正需要你判断的只有几个字段。name是全局唯一的引用名,后面集群里写的就是它;url里一定要带serverTimezone,否则时间字段跨时区会偏 8 小时,这个坑排查起来极其费劲;minCon和maxCon决定这个实例上常驻和峰值连接数,把它乘以实例数、再乘以集群数,就是你的 MySQL 端要扛的总连接数,max_connections不够就会在压测时突然大面积超时;maxConnectTimeout是建连超时,内网 3000 毫秒够用,跨机房要放大;maxRetryCount是建连失败重试次数,从库不稳的时候它会救你一命,但也会让错误暴露得慢一点。
instanceType用来标记这个实例的角色,weight是读流量权重,多从库分摊读的时候用得上。其余字段保持样例默认值就行,别一上来就全改一遍——出问题的时候你不知道是哪个字段改坏的。
3.3 集群:masters 和 replicas 决定读写去哪
集群层是很多人第一次用会写错的地方。masters里放的是写节点,replicas里放的是读节点,两个列表里填的都是数据源的name,不是主机名。
{ "name": "c0", "clusterType": "MASTER_SLAVE", "masters": ["dw0"], "replicas": ["dw1", "dw2"], "readBalanceType": "BALANCE_ALL", "switchType": "SWITCH", "heartbeat": { "replicas": "SELECT 1", "timeout": 1000 } }参数上有几个边界值得说清楚。masters只有一个元素时就是标准主从;如果放多个,它表达的是多个写分片,路由会再按分片规则分配,别把它当成"双主同时写"。readBalanceType控制读请求怎么分摊到 replicas,全都可用的策略吞吐最好,但对主从延迟最敏感;如果业务里有"写完立刻查"的场景,要么把读策略调保守,要么让这类查询走事务,别硬扛。switchType是主库挂了要不要自动切,测试环境建议关掉,免得排查问题时主从角色自己变来变去,日志都看不懂。heartbeat是探活语句和超时,探活语句别写太重的 SQL,SELECT 1足够。
3.4 逻辑库:应用看到的那个"库"
逻辑库是应用侧唯一感知到的一层,名字最好和原来的物理库名保持一致,这样老代码里的库名不用动。
{ "schemaName": "shop", "targetName": "c0", "normalTables": {}, "globalTables": {}, "shardingTables": {}, "customTables": {} }targetName指向集群名。下面四个空对象是四类表的容器,用哪类表就往哪类里填:normalTables是不分片的普通表,globalTables是每个节点都存一份全量数据的字典表(省份、类目、配置项这种),shardingTables是分片表,customTables是你要自己接管路由的表。新手最常见的误操作是把所有表都塞进shardingTables,结果字典表被切得七零八落,join 的时候疯狂跨节点。
3.5 用命令在线建:三条 SQL 顶三个文件
除了改文件,也可以在客户端里直接建,适合临时加一个从库验证效果。命令写在/*+ */注释里,末尾的分号不能少。
-- 建数据源:字段和 json 文件里的一一对应 /*+ mycat:createDataSource{ "name":"dw3", "url":"jdbc:mysql://127.0.0.1:3307/db1?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai", "user":"root", "password":"123456", "minCon":2, "maxCon":50 } */; -- 建集群:只有一个写节点时,replicas 可以留空 /*+ mycat:createCluster{"name":"c1","masters":["dw3"]} */; -- 建逻辑库:挂到刚建的集群上 /*+ mycat:createSchema{"schemaName":"shop2","targetName":"c1"} */; -- 让内存里的元数据重新加载一遍 /*+ mycat:reload{} */;逻辑上要理解一件事:这几条命令改的是运行态元数据,同时会往 conf 目录写对应文件,所以它和手改文件是同一个数据源,别两边同时改同一个对象。参数上和配置文件完全一致,唯一要注意的是 JSON 必须写在一行内或者保证注释完整透传——有的客户端会"帮你"把注释删掉,一删命令就退化成普通 SQL,报一个莫名其妙的语法错误。
4. 分片表落地:shardingTables 骨架、分片键选型与落位验证
单库单表跑通之后,真正的工作量在分片表上。分片表的本质是"一条逻辑表名 + 一套路由函数 + 一组物理节点",三者必须严格对齐:节点数要和路由函数的参数匹配,建表语句要和物理库里的实际结构一致。这两条对不上,表现是数据能插进去但查不出来,或者干脆建表就报错。
4.1 分片表在配置里的骨架
分片表挂在逻辑库 json 的shardingTables下面,键是逻辑表名。
"shardingTables": { "order_detail": { "createTableSQL": "CREATE TABLE `order_detail` (`id` bigint NOT NULL, `order_id` bigint NOT NULL, `user_id` bigint NOT NULL, `amount` decimal(12,2) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "function": { "clazz": "io.mycat.router.mycat1xfunction.PartitionByLong", "name": "partitionByLong", "properties": { "partitionCount": "2", "partitionLength": "2" } }, "shardingNodes": ["c0_0.order_detail", "c0_1.order_detail"] } }四个字段各有分工。createTableSQL是建表模板,代理在初始化时按它去物理库建表,所以它必须和你线上真实表结构逐字一致,包括字符集和索引——线上表加了个字段忘了同步这里,重启之后新节点建出来的表就少一列。function.clazz是路由算法类,从 lib 目录里包里自带的那批io.mycat.router.开头的类里选,除非你非常清楚自己在做什么,别自己写一个。function.properties的键名跟着算法走:取模类算法一般要分片键列名和分片数,范围类算法一般要列名和区间边界,具体以包里样例为准。
shardingNodes的写法要特别注意,它是"节点名.表名",而节点名的形式取决于你的集群怎么定义:单主从集群里直接写数据源名(dw0.order_detail),多写节点的分片集群里是集群名加序号(c0_0.order_detail、c0_1.order_detail)。这两种写法在不同小版本、不同集群形态下不一样,最稳的做法是打开 conf/schemas 下自带的样例文件,照着它的形式写,别凭记忆敲。
4.2 分片键和算法怎么选
选算法之前先看查询长什么样,别看数据长什么样。分片键必须是绝大多数查询都会带上的那个条件,否则每次查询都要广播到所有分片,代理层的优势直接归零。
| 算法类型 | 适合的分片键 | 扩容代价 | 典型翻车点 |
|---|---|---|---|
| 取模/哈希 | 基数高、写入分散,如 user_id、order_id | 高,改分片数要整体重分布 | 分片键为 NULL 或字符串大小写不一致,落到默认节点 |
| 范围/区间 | 有时间语义,如月份、号段 | 低,加区间即可 | 冷热不均,新数据全压最后一个分片 |
| 自定义 | 节点数会频繁变动的场景 | 中 | 算法类改完 reload,存量数据路由对不上 |
我自己总结的三条硬标准:第一,分片键基数要足够大,性别、状态这种字段分片等于没分;第二,核心查询的 where 里必须带分片键,报表类全表扫描的查询要单独规划走不走代理;第三,分片键一旦写入就不要更新,跨分片改分片键等于一条 update 变两条跨库事务,代理层帮不了你。三条都满足再动手配,比配完再改省事得多。
4.3 三步验证数据真的落对了
配置写完,reload 之后必须验证,别信"没报错就是对的"。验证分三步:从代理侧写数据、从代理侧读回来、再到每个物理库对数。
# 第一步和第二步:从代理写入并读回,走的是应用的视角 mysql -h127.0.0.1 -P8066 -uroot -p123456 shop -e " insert into order_detail(id, order_id, user_id, amount) values (1,1001,1,9.90),(2,1002,2,19.90),(3,1003,3,29.90); select count(*) as proxy_total from order_detail;"# 第三步:到每个物理库分别数一遍,两边的和要等于代理上看到的总数 for p in 3306 3307; do mysql -h127.0.0.1 -P$p -uroot -p123456 -N -e \ "select '$p' as port, count(*) from db1.order_detail;" done第一段的逻辑是确认代理层的读写链路是通的,proxy_total是代理聚合后的结果。第二段的关键在于"分布"而不是"总数":如果两个物理库各 3 行、但某个库里出现了本该属于另一个分片的分片键,说明路由函数参数写错了;如果总数对、但其中一个是 0,说明shardingNodes只写了一个节点或者分片数配成了 1。这种错误不会报异常,只会在数据量涨起来之后以"某个库磁盘爆了"的形式暴露,所以每次改分片配置都要跑一遍。
4.4 哪些表不该分片
字典表、配置表、类目表这类数据量小、读多写少、又经常被 join 的表,放globalTables里,每个节点存一份全量,join 就在本地完成。业务主表和它的明细表如果总是按同一个维度查(订单和订单明细都按 user_id 查),把两张表的shardingNodes一一对齐,保证同一份数据落在同一个节点,join 就不会跨库。
反过来,任何需要跨分片聚合的查询都要提前想清楚:count(*)、sum、深分页、多表 join,在代理层的代价远高于单库。这些查询要么在业务侧改成按分片键查、要么单独走一条不经过代理的报表链路,别指望中间件替你解决。
5. 避坑与排查:五个高频翻车现场
下面这五条是我自己在不同环境里反复遇到的,按"现象 → 原因 → 解决"写,遇到问题时可以按顺序对照。
5.1 启动
本文还有配套的精品资源,点击获取