☰
Docker部署Mycat:实现MySQL读写分离与主从复制实战
2026/10/15 6:57:47 网站建设 项目流程

前几天一个后端朋友跑来找我,说他们的订单库最近扛不住了:单台 MySQL 读写混跑,查询一多,CPU 直接飙到 80%,关键业务写操作被拖得特别慢。这个场景太典型了,读多写少、数据库连接动不动被慢查询占满。当时我建议他走读写分离,把查询流量从主库上拆出去。而实现手段,我推荐了 Mycat——这个系列前面已经聊过它的基本定位,今天这篇就接着往下走,用 Docker 把 Mycat 跑起来,配合 MySQL 主从复制,把读写分离完整落地。

这篇内容适合两类人:一类是后端开发,想在不改业务代码的情况下给数据库减负;另一类是初级 DBA,想弄明白 Mycat 到底怎么配置、流量怎么分流的。文章中我会把 MySQL 主从搭建、Mycat 三份核心配置文件、启动验证、常见坑全部过一遍,配置基本可以直接抄走用。先提醒一句,读写分离不是银弹,后面我会讲清楚它解决的问题和它解决不了的问题。

1. 先想清楚:为什么需要 Mycat 这种中间件来读写分离

1.1 单库瓶颈到底出在哪里

很多团队对数据库的第一反应是“加配置”“上 SSD”,但字节到硬件之后,真正的问题往往不在磁盘,而在并发。以电商订单库为例,一个下单操作可能只写几条记录,但商品详情、订单查询、用户历史订单这些接口,一次请求可能就要查几十上百条记录。读和写混在同一个实例上时,大量 select 会把 CPU、IO 和连接池打满,写请求就得排队。

这就是“读多写少”的典型场景。读写分离的核心思路说白了就一句话:把那些慢查询、查询量大的 select 全部引流到从库,主库只保留写操作和一小部分实时性要求极高的读操作。这样主库压力下降,从库专门扛读流量,整体吞吐能提高不少。

但这里有个前提必须说清楚:从库的数据是异步同步过来的,存在主从延迟。如果业务要求“写入后立刻读到自己写的数据”,读写分离就会出问题。所以做任何方案之前,先问自己一句:这个系统能容忍 1-3 秒的数据不一致吗?能容忍,才走读写分离这条路。

1.2 Mycat 的角色:应用无侵入的“数据库路由器”

实现读写分离,常见有两条路。一条是应用层搞多数据源,比如在代码里写两个数据源,然后通过 AOP 或注解给读操作指定从库;另一条是引入中间件,让应用只连一个地址,中间件自己完成 SQL 解析和路由。两条路我都走过,前者灵活但侵入性强,每个项目都要维护一套切库逻辑;后者对业务透明,改动最小。

Mycat 走的就是后者。它在 MySQL 前端伪装成一个数据库服务器,应用根本不知道背后连着两台甚至更多台 MySQL。Mycat 收到 SQL 后,解析出是 select 还是 insert/update/delete,再决定把这包 SQL 扔给哪个后端实例。从应用视角看,它只是换了一个“数据库地址”而已。

“数据库路由器”这个比喻我个人觉得很贴切。就像快递分拣中心,包裹(SQL)到了之后,分拣员看目的地址(SQL 类型),放到不同线路的车上,送到对应片区(主库或从库)。快递员(应用)不需要关心线路怎么走,他只需要把包裹交给分拣中心。

1.3 为什么直接用 Docker 部署

我同事第一次部署 Mycat 是在物理机上跑的,结果踩了一下午环境坑:JDK 版本不匹配、启动脚本路径不对、配置文件少拷贝了一个、端口被别的 Java 进程占掉……Mycat 本质是一个 Java 进程,对运行环境有一定要求。用 Docker 之后,镜像里把 JDK 和运行环境全部打包好了,拉下来直接启动,省掉一大半环境问题。

另外,Mycat 在测试环境和生产环境往往需要不同配置,容器化之后配置目录独立挂载,改配置、重启容器就能完成环境切换。集群扩展时,多启几个容器也能快速横向拉起。

当然,容器化不是没代价。Mycat 本身不存数据,但它依赖配置文件和后端连接,所以配置目录必须挂载到宿主机,镜像升级时不要覆盖配置;容器重启策略也要想好,别让容器一崩就“消失”了。下面这张表可以看下容器部署和传统部署的差别:

对比项物理机/虚拟机部署Docker 部署
环境一致性依赖基础环境,易受 JDK 版本干扰镜像内容自带环境,拉起一致
配置管理散落在安装目录,迁移易遗漏独立挂载,可备份可复用
多实例扩展端口、目录冲突风险高容器隔离,扩展方便
快速恢复需要重新部署一次一条命令重启

2. 地基先行:用 Docker 搭好 MySQL 主从复制

2.1 主从复制的核心链路

Mycat 只负责路由,真正的数据同步还得靠 MySQL 自己的主从复制。先花 30 秒把原理理清楚,后面排查问题会轻松很多。

主库上所有数据变更都会按顺序写进 binlog。从库启动两个线程:I/O 线程负责连上主库,把 binlog 拉到本地存成 relay log;SQL 线程负责把 relay log 里的日志逐条回放,做到和主库一样的数据变更。说白了,主从复制就是“日志传输 + 日志回放”。

要注意的点是:这个同步是异步的,从库追日志需要时间。正常情况下延迟很小,几十毫秒;但主库有大事务或者从库配置较弱时,延迟会被拉大。后面接 Mycat 时,读请求是按批打到从库的,如果从库明显落后,读出来的数据就是旧数据,这一点业务方必须有心理预期。

2.2 启动两个 MySQL 容器

假设宿主机已经装好 Docker,我们先创建目录,放主从各自的自定义配置文件。

mkdir -p /data/mysql-master/conf /data/mysql-master/data mkdir -p /data/mysql-slave/conf /data/mysql-slave/data

主库配置文件/data/mysql-master/conf/my.cnf:

[mysqld] server-id=1 log-bin=mysql-bin binlog_format=ROW

从库配置文件/data/mysql-slave/conf/my.cnf:

[mysqld] server-id=2 relay-log=mysql-relay-bin read_only=1

server-id必须唯一,等于是实例在集群里的身份证。主库开启log-bin才可能产生 binlog;从库设置read_only,防止业务误连从库写数据。binlog_format 我用 ROW,它比 STATEMENT 在同步准确性上更好,虽然日志体积会大一些,但对一致性友好。

然后启动两个实例,端口故意错开:

docker run -d --name mysql-master \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -v /data/mysql-master/conf:/etc/mysql/conf.d \ -v /data/mysql-master/data:/var/lib/mysql \ mysql:8.0 docker run -d --name mysql-slave \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -v /data/mysql-slave/conf:/etc/mysql/conf.d \ -v /data/mysql-slave/data:/var/lib/mysql \ mysql:8.0

启动后分别确认能登录:

docker exec -it mysql-master mysql -uroot -proot123 docker exec -it mysql-slave mysql -uroot -proot123

两个容器之间能互相访问吗?这个先别急着测。目前它们都在同一个默认 bridge 网络里,容器名不一定能直接解析。后面 Mycat 容器要访问这两个 MySQL,我建议给 Mycat 用宿主机 IP 去连,省去网络解析的麻烦。如果跨服务器部署,就用对应服务器的 IP。

2.3 初始化主从同步

在主库执行,创建同步专用账号:

CREATE USER 'repl'@'%' IDENTIFIED BY 'repl123'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES;

这个账号权限只需要REPLICATION SLAVE,不需要给库表读写权限,权限最小化。

然后查看主库当前 binlog 位点:

SHOW MASTER STATUS;

记录下File和Position,比如mysql-bin.000001和157。接着到从库执行:

CHANGE MASTER TO MASTER_HOST='192.168.1.10', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='repl123', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=157; START SLAVE;

这里的MASTER_HOST要根据实际环境写,容器内可以用宿主 IP。执行完检查状态:

SHOW SLAVE STATUS\G;

重点关注这几个字段:

  • Slave_IO_Running: Yes,表示能连上主库并拉日志;
  • Slave_SQL_Running: Yes,表示 relay log 回放正常;
  • Seconds_Behind_Master: 0,表示当前没有延迟。

最后验证一下同步是否真的通了。主库建库建表、插入数据:

CREATE DATABASE db_test; USE db_test; CREATE TABLE t_user (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); INSERT INTO t_user(name) VALUES ('zhangsan'),('lisi');

到从库执行SHOW DATABASES;和SELECT * FROM db_test.t_user;。能看到db_test和两条数据,说明主从链路没问题。

2.4 这个阶段最容易翻车的三个点

第一,从库配置了server-id=2,但auto.cnf里的server-uuid如果跟主库一样,复制会直接报错。这种情况常见于用docker cp或者直接复制了主库的数据目录。解决方法很简单,删掉从库数据目录下的auto.cnf再重启容器。

第二,MySQL 8 默认的认证插件是caching_sha2_password,老版本 MySQL 客户端和很多中间件解析不了。尽管 Mycat 这一步还没上,我建议在主从搭建时就把账号认证方式统一一下。后续 Mycat 要连 MySQL 时,用下面这种写法创建账号:

CREATE USER 'mycat'@'%' IDENTIFIED WITH mysql_native_password BY 'mycat123'; GRANT ALL PRIVILEGES ON db_test.* TO 'mycat'@'%'; FLUSH PRIVILEGES;

第三,忘记放行防火墙端口。两台机器部署时,从库要能访问主库的 3306,后面 Mycat 机器要能访问 3306、3307。安全组和防火墙规则提前确认。

3. Mycat 配置拆解:三份 XML 是全部核心

3.1 先建立逻辑库和逻辑表的概念

Mycat 配置里经常出现“逻辑库”“逻辑表”这个词。所谓逻辑,就是 Mycat 虚拟出来的库表,应用只会看到这一层。比如你定义了一个逻辑库叫TESTDB,里面的逻辑表叫t_user,但背后 Mycat 真正访问的其实是你 MySQL 里的物理库db_test、物理表t_user。

可以理解成前台点菜:客户(应用)对着菜单(逻辑库表)点菜,后厨(MySQL)真正做菜,菜单上写的是“鱼香肉丝”,后厨做的可能用的是猪肉而不是鱼肉。只要菜品符合预期,客户不需要知道后厨细节。

Mycat 的核心配置就是三份 XML:server.xml定义账号和全局参数,schema.xml定义逻辑库表和后端数据源的关系,rule.xml定义分片规则。读写分离场景下,前两份是绝对主角。

3.2 server.xml:登录账号和全局参数

先看一份最小可用的server.xml:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mycat:server SYSTEM "server.dtd"> <mycat:server xmlns:mycat="http://org.opencloudb/"> <system> <property name="defaultSqlParser">druidparser</property> </system> <user name="mycat"> <property name="password">mycat123</property> <property name="schemas">TESTDB</property> </user> <user name="manager"> <property name="password">manager123</property> </user> </mycat:server>

第一个user是给应用连接 Mycat 用的,连接后只能看到TESTDB这个逻辑库。第二个user是管理端口用的,登录 Mycat 的 9066 管理端口做运维操作,比如查看数据源状态、在线调整配置。

这里有个细节容易被忽略:Mycat 的前端连接 8066 和管理连接 9066 是同一个进程的两个端口,配置里的用户体系是独立的。生产环境里管理账号要单独设置,不要共用业务账号。

3.3 schema.xml:读写分离的真正战场

schema.xml是 Mycat 的灵魂。读写分离能不能生效,主要看dataHost里的writeHost和readHost配得对不对。下面这份配置可以直接用于我们的主从环境:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://org.opencloudb/"> <schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100"> <table name="t_user" primaryKey="id" dataNode="dn1"/> </schema> <dataNode name="dn1" dataHost="localhost1" database="db_test"/> <dataHost name="localhost1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1"> <heartbeat>select user()</heartbeat> <writeHost host="master" url="192.168.1.10:3306" user="mycat" password="mycat123"> <readHost host="slave" url="192.168.1.11:3307" user="mycat" password="mycat123" weight="1"/> </writeHost> </dataHost> </mycat:schema>

逐个解释关键属性:

schema标签定义了逻辑库TESTDB,table标签把逻辑表t_user映射到数据节点dn1。dataNode标签说明这个逻辑节点对应的是dataHost上的物理库db_test。如果后续有多个表,可以继续在schema里加table标签。

dataHost里balance是最关键的参数。balance="0"表示不开启读写分离,所有请求走写节点;balance="1"表示读请求全部走从库节点;balance="2"和"3"主要用在多分片或多主多从场景。读写分离最常用balance="1"。

writeHost和readHost是嵌套关系,readHost必须写在writeHost内部。Mycat 的读写分离不是平行的两台机器,而是“写主机下挂读机器”的树形结构。如果读机器和写机器配置在同一层,Mycat 会认为它们是两个数据源,和预期就偏离了。

heartbeat是心跳检测语句,Mycat 用它定期探测后端 MySQL 是否存活。探活失败后,如果switchType="1",Mycat 会自动把写流量切换到备用节点。心跳语句不复杂,select user()足够用。

3.4 rule.xml 和分片的关系

如果你只做读写分离,不需要配置分片规则。但 Mycat 启动时依然会读取rule.xml,所以我建议保留一份默认配置,不要删除。里面定义的分片算法、分片函数,在schema.xml里没有表指定分片规则时基本不生效,可以不用深究。

如果硬要精简,可以把rule.xml换成最简形式,只保留空的<mycat:rule>标签结构。我这里直接沿用默认配置,更稳妥。

4. Docker 运行 Mycat 全程实操

4.1 准备镜像和配置目录

我用的镜像是基于 Mycat 1.6.7.6 版本打的容器镜像,镜像里默认路径为/usr/local/mycat/conf。不同镜像的目录可能会变,启动之前可以先执行docker run --rm 镜像名 ls /usr/local/mycat看一下结构,确认 conf 目录位置。

提前把三份配置文件放到宿主机/data/mycat/conf:

mkdir -p /data/mycat/conf # 将 server.xml、schema.xml、rule.xml 放入 /data/mycat/conf

然后启动 Mycat:

docker run -d --name mycat \ -p 8066:8066 \ -p 9066:9066 \ -v /data/mycat/conf:/usr/local/mycat/conf \ mycat-image:1.6-7.6

注意端口映射:8066 是应用连接端口,9066 是管理端口。两个端口都要映射出来。

启动之后看日志确认状态:

docker logs -f mycat

出现类似startup success或Server startup successfully的日志,说明 Mycat 已经起来了。如果日志里报读取配置文件失败,基本是挂载路径没对或者 XML 语法有问题。

4.2 用客户端走一遍读写链路

用 MySQL 客户端连接 Mycat 的前端端口:

mysql -u mycat -pmycat123 -h 127.0.0.1 -P 8066

登录后执行:

SHOW DATABASES; USE TESTDB; SELECT * FROM t_user;

能正常执行,说明 Mycat 和后端 MySQL 的连接也通了。此时你可能会发现,查出来的数据全靠主从复制同步到从库。这个没有任何问题,因为 Mycat 已经替应用做好了路由,应用只需要关心TESTDB这个逻辑库。

4.3 验证读写分离是否真正生效的三种方法

很多人的测试停在了“能连上、能查到数据”,但这不够。我们要确认 select 是不是真的走了从库。

方法一:反复执行服务器变量查询

在 Mycat 前端连接里执行:

SELECT @@server_id;

如果每次返回 2,说明读请求全部路由到了从库(server-id=2);如果偶尔返回 1,说明有部分读请求还留在主库。在balance="1"配置下,稳定返回 2 是符合预期的。

方法二:开启从库的 general_log 看真实流量

最可靠的方法是直接看后端 MySQL 收到的 SQL。在主库执行:

SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';

然后在 Mycat 上执行几条 select。再到主库查:

SELECT * FROM mysql.general_log WHERE argument LIKE 'SELECT%' ORDER BY event_time DESC LIMIT 5;

这个方法能精确看到哪些 SQL 打到了哪个实例。实际操作时,我一般是把主库和从库的 general_log 都打开,对比同一批 select 分别出现在哪个库的日志里。

方法三:通过管理端口查看连接分布

连接 Mycat 的 9066 管理端口:

mysql -u manager -pmanager123 -h 127.0.0.1 -P 9066

执行:

SHOW @@DATASOURCE;

输出里能看到每个dataHost的 writeHost 和 readHost 各自有多少连接。如果只看到 writeHost 有连接,readHost 一直是 0,多半是配置没对或者根本没触发读请求。

4.4 验证时顺手做的数据一致性测试

验证读写分离的同时,我建议顺手测一下主从同步的正常性。在 Mycat 上执行:

INSERT INTO t_user(name) VALUES ('wangwu'); SELECT * FROM t_user;

接着到从库查:

SELECT * FROM db_test.t_user;

如果能查到wangwu,说明写 MySQL 主库后,同步到从库的链路也正常。不要只验证select走了从库,要验证写入后的数据最终能流到从库,这样才算一条完整的闭环。

5. 踩坑记录和问题速查

5.1 连不上 8066 端口

第一个常见现象是mysql -h 127.0.0.1 -P 8066报连接拒绝。我遇到的情况有一半是 Mycat 容器根本没好,另一半是端口映射没配。

先看docker ps,确认容器在跑;再看docker logs mycat,确认没有报异常。如果容器正常但宿主机端口不通,检查防火墙和 SELinux。容器内部端口 8066 是否监听,可以用docker exec mycat netstat -tlnp | grep 8066确认。如果容器内部没问题但宿主机访问不到,考虑端口映射是不是被其他进程占了。

5.2 数据全部打在主库上

这种情况最打击信心:配置看着没问题,但读请求全都走了主库。

按优先级排查:

  • 看schema.xml里dataHost的balance是不是0,如果是,改1重启;
  • 检查<table>标签是否真的把逻辑表映射到了对应dataNode,如果表没有指定,Mycat 会走默认路由;
  • 确认readHost的账号密码对不对,如果从库连接失败,Mycat 可能直接把流量降级到 writeHost;
  • 最后,检查是不是事务内读。Mycat 对事务内 SQL 有特殊处理——事务期间的所有 SQL 都会发给 writeHost,保证一致性。业务如果使用了BEGIN之后查数据,这些 select 走主库是正常现象,不算 bug。

一般事务内读会有大量 select 走主库,这个可以通过代码层面优化,减少长事务。

5.3 MySQL 8 认证插件不兼容

这个问题在 5.0 和 8.0 混用时特别典型。Mycat 自带的后端驱动可能不支持 MySQL 8 的caching_sha2_password认证,表现为日志里报Unknown authentication plugin。

解决办法不是升级驱动,而是在 MySQL 里给连接账号改认证方式:

ALTER USER 'mycat'@'%' IDENTIFIED WITH mysql_native_password BY 'mycat123';

改完不用重启 mycat,只要后端连接池重建连接就能生效。重建连接最快的方式是重启 mycat 容器。

5.4 读写分离期间必须盯的业务细节

主从延迟是最容易忽略的。balance="1"把所有 select 都推到从库后,一旦从库延迟超过几秒,可能会出现这种场景:用户下单成功后查订单详情,列表里看不到刚下的单,然后工单就来了。

经验做法是写一个延迟监控脚本,定时从从库执行SHOW SLAVE STATUS,把Seconds_Behind_Master打到监控系统,超过阈值直接告警。同时,业务侧尽量做兜底:关键数据写入后立刻读用缓存或者强制走主库,普通列表查询可以容忍延迟。

还有连接池的问题。Mycat 本身有连接池,应用也有连接池。如果应用连接池里的老连接一直没释放,Mycat 又发生过后端切换,可能会让请求打到旧节点。遇到诡异的数据“看不新鲜”,可以先考虑刷新连接池试试。

下面整理了一张速查表,方便你后面直接对着排查:

现象可能原因排查命令/动作
8066 端口无法连接容器未启动、端口映射错误、防火墙docker logs mycat、netstat -tlnp
只有主库有流量balance=0、readHost 配错查看 schema.xml、SHOW @@DATASOURCE
报认证失败MySQL8 认证插件不兼容ALTER USER ... mysql_native_password
数据查不到最新的主从延迟SHOW SLAVE STATUS看Seconds_Behind_Master
从库能连但读写未分离事务内读、长连接已建立检查应用是否BEGIN后再 select
Mycat 启动失败XML配置错误、挂载路径错误看容器日志,确认 conf 目录挂载位置

最后分享个人习惯

这套东西我前前后后搭过不少次,现在每次做读写分离方案,都会固定做三件事:一是部署前先单独验证主从同步,确保数据能同步才开始接 Mycat,否则后面排查问题会多一重变量;二是验证阶段一定开 general_log,看实际SQL打到了哪个实例,而不是只看SELECT @@server_id——后者能说明大致方向,前者才是铁证;三是给主从延迟设监控和告警,这是读写分离上线后最容易出事的点,延迟一大,业务反馈立刻就来。

如果你接下来要在这个环境上继续扩展,可以往两个方向走:一个是在 Mycat 上做水平分片,把单表数据拆到多个 MySQL 实例;另一个是给 writeHost 加一个备用主节点,配合switchType做自动故障切换。从读写分离到分片,到高可用,这些东西都是一层一层叠上去的,每一层先跑稳,再往上加,比一次性搞一个大而全的方案要可靠得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询