☰
MySQL主从复制与Mycat读写分离实践:从单库瓶颈到高并发架构
2026/10/22 16:33:29 网站建设 项目流程

半夜被报警电话叫起来,看了一眼监控:数据库CPU 92%,活跃会话飙到300多,慢查询日志每分钟刷几十页。登录上去查了下top SQL,发现全是同一类查询——订单列表、商品详情这种纯读接口,写操作占比其实不到两成。这是典型的读多写少场景,单库已经扛不住了。当时我们做的调整就是:MySQL做主从复制,再用Mycat做读写分离,把读流量从主库切走。这篇文章把整个实践过程写清楚,从主从搭建到Mycat配置,再到验证手段和踩坑记录,希望能给同样卡在单库瓶颈的朋友一条能直接照做的路径。

1. 为什么单库会先扛不住:读写分离的动机拆解

1.1 压力不是平均分配的

一个常规业务系统,表结构几十上百张,QPS到几千的时候,最先报警的往往是数据库。写路径其实还好,一次insert或者update再慢也就几毫秒,但读完全不是一回事。一个大列表查询可能扫几万行,一个带聚合统计的报表可能要全表遍历。随着数据量增长,MySQL的buffer pool命中率下降,磁盘随机IO上来了,CPU和IO同时吃紧。这时候翻慢查询,清一色是select。

我见过很多团队第一反应是加内存、换SSD、升CPU,整机配置堆上去之后确实能扛一阵子,但单机的天花板就在那里。你不可能无限升级硬件,尤其是数据量还在涨的情况下,升级的钱烧得很快,而且MySQL实例本身还有连接数上限、磁盘吞吐上限这类硬约束。真正合理的思路不是把单机养得更肥,而是把流量拆开,让不同的机器各司其职。

1.2 中间件在数据库架构里的位置

流量拆分的经典做法是读写分离:主库继续负责写入,从库通过主从复制拿到几乎相同的数据,专职处理读请求。但问题来了——应用怎么知道哪台是主、哪台是从?如果让每个服务自己维护两套数据源,主从切换的时候怎么办?扩一台从库是不是还要改应用配置?这些脏活累活,交给数据库中间件来做是最省心的。

MySQL生态里,Mycat就是干这个的。它代理MySQL协议,对外暴露一个逻辑库,应用只连这一个入口;对内管理真实的物理MySQL节点,决定每个SQL应该分给谁。读写分离是它最基础也最常用的能力之一。应用完全感知不到后端的拓扑变化,主从地址变了、从库加了一台,都只改Mycat的配置文件,业务代码一行不动。

1.3 引入中间件的收益和代价

好处很直观:应用不用感知主从地址变化;从库扩容只改配置;读请求可以按轮询或者权重分发;后面真要上分片,应用也几乎不用改代码。

代价同样需要说清楚。多了一层网络转发,查询链路变长了,单次请求的延迟会略微上升;多了一个运维对象,Mycat本身需要监控、需要升级、需要排障;排查问题的时候,你得多看一个环节。用不用中间件,取决于团队规模和系统阶段。如果只是几十个接口的小系统,应用层写个读写分离工具类就够了;但到了需要横向扩展、有专职DBA或者基础设施人员的阶段,中间件带来的管理收益会明显大于这点延迟成本。

2. 动手前必须搞懂的三个核心概念

2.1 主从复制是读写分离的地基

读写分离不会凭空产生数据副本,它只是把读请求路由到从库,而从库的数据来源于MySQL主从复制。如果主从复制跟不上,读写分离就是空中楼阁。

主从复制的原理,通俗讲就是主库把每一次变更记录到binlog,从库有一个IO线程负责去主库拉binlog,写进自己的relay log,然后SQL线程再把这些日志在本地回放一遍。主库在记流水账,从库照着流水账重新执行。配置主从有几个关键点绝对不能错:主库必须开启binlog;每一台实例的server-id必须全局唯一;binlog_format建议用ROW或者MIXED;复制账号必须有REPLICATION SLAVE权限。这些我在第三部分都会给出具体操作。

2.2 逻辑库、数据节点、物理库的映射关系

Mycat把真实的MySQL实例藏在身后,应用看到的是一个"逻辑库"。这个逻辑库在Mycat的schema.xml里定义为schema,它下面的一张表,可能落在某一个物理节点,也可能按规则散落在多个物理节点。

每个物理节点在Mycat里叫dataNode,一个dataNode对应"某一台MySQL实例上的某一个数据库"。dataNode挂在dataHost上,dataHost里才是真实的MySQL地址。读写分离场景下,dataHost里面会有writeHost和readHost两种节点:writeHost对应主库,readHost对应从库。理解这条链路——schema到dataNode到dataHost再到writeHost/readHost——后面配置就不会乱。

2.3 三个决定读写行为的参数:balance、writeType、switchType

这几个参数是整个读写分离配置的灵魂,先记清楚。

参数取值含义
balance0不开启读写分离,所有读都走主库
balance1读请求全部发到readHost,主库只处理写
balance2读请求在writeHost和readHost之间分发
balance3读请求只发到readHost,语义上主库完全不参与读
writeType0所有写请求发到第一个writeHost
switchType-1不自动切换主从
switchType1自动切换,主库故障时把写切到从库

做读写分离,我最常用的就是balance="1",语义最干净:主库专注写入,从库专注读取,互不干扰。balance="2"适合只有一台主一台从但不想浪费主库读性能的场合,但主库同时承担读和写,压力还是会混在一起。至于switchType,我建议刚开始搭建的时候先用-1,把读写分离跑通了再研究自动切换,不然主库一挂,切换逻辑和数据一致性同时出问题,排查起来非常痛苦。

3. 环境准备:MySQL主从双机搭建实录

3.1 节点规划与版本选择

先规划三台节点,我这次用的是虚拟机的示例地址:

角色主机地址用途
MySQL主库192.168.56.101读写分离中的写节点
MySQL从库192.168.56.102读写分离中的读节点
Mycat中间件192.168.56.103代理入口

MySQL版本我建议用5.7系列的稳定版本。如果你非要用8.0,要注意MySQL 8.0默认的认证插件是caching_sha2_password,Mycat 1.6系列的兼容性并不好,需要把账号改成mysql_native_password才行。这个坑我后面在排查部分还会提到。

3.2 主库侧配置

主库的my.cnf在[mysqld]段下增加以下配置:

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

gtid_mode和enforce_gtid_consistency可以一起打开,后面做故障切换、重搭从库的时候,定位复制位点会方便很多。配置完重启MySQL服务。

然后创建两个账号,一个给复制用,一个给Mycat连接用:

-- 复制账号 CREATE USER 'repl'@'%' IDENTIFIED BY 'ReplPass123'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; -- Mycat连接账号 CREATE USER 'mycat'@'%' IDENTIFIED BY 'YourPass123'; GRANT SELECT, INSERT, UPDATE, DELETE ON testdb.* TO 'mycat'@'%';

注意Mycat账号至少要给增删改查权限,如果业务里会执行DDL、创建临时表这类操作,权限还要相应放宽。权限给漏了,后面从库查询会直接报access denied,排查起来容易走弯路。

3.3 从库侧配置

从库的my.cnf和主库只有一点不同:

[mysqld] server-id=2 read_only=1

read_only建议加上。它只限制普通账号的写操作,复制线程回放binlog不受影响,可以在很大程度上防止从库数据被误改。

然后执行复制初始化:

CHANGE MASTER TO MASTER_HOST='192.168.56.101', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='ReplPass123', MASTER_AUTO_POSITION=1; START SLAVE;

这里用了MASTER_AUTO_POSITION=1,也就是GTID自动定位,是基于刚才主库开启gtid_mode的前提。如果你没开GTID,就要改成用MASTER_LOG_FILE和MASTER_LOG_POS手动指定位点。

3.4 验证主从是否真正跑通

验证命令很简单,在从库执行:

SHOW SLAVE STATUS\G

重点看三个值:Slave_IO_Running必须是Yes,Slave_SQL_Running必须是Yes,Seconds_Behind_Master最好是0。

再做一个最直观的验证:在主库insert一条测试数据,到从库去select,能查到就说明复制链路通了。如果Slave_IO_Running显示Connecting,排查顺序是:主库防火墙有没有放行3306、复制账号的host限制是否正确、两端server-id是不是重复了。这三条占了九成以上的IO线程连不上的原因。

4. Mycat核心配置逐行拆解

4.1 安装与目录结构

Mycat解压之后,目录结构并不复杂,重点记住三个地方:conf目录放配置文件,logs目录放运行日志,bin目录是启动脚本。部署前先确认机器上的JAVA版本和Mycat要求的版本匹配,版本不匹配会出现各种莫名其妙的启动失败。

日志这块提前熟悉一下,后面排查全靠它。wrapper.log记录JVM启动过程,mycat.log记录运行期业务日志,console.log是标准输出。启动出了问题,先翻wrapper.log,基本能定位到原因。

4.2 server.xml:Mycat的账号与逻辑库绑定

server.xml里配置的是连接Mycat的账号。我用的是:

<user name="mycat"> <property name="password">Mycat123</property> <property name="schemas">testdb</property> <property name="readOnly">false</property> </user>

password是应用连Mycat时用的密码,schemas指定这个账号能访问哪些逻辑库。这里要特别提醒:Mycat只是路由层,它不会替代MySQL做权限校验。Mycat账号能不能查数据,最终还是看后端MySQL里mycat账号的真实权限。两层权限都要配好,缺一个都不行。

4.3 schema.xml:读写分离的核心配置

这是整个实践里最重要的文件。我用一套最简配置把链路串起来:

<mycat:schema xmlns:mycat="http://org.mycat.ip.configure"> <schema name="testdb" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1"/> <dataNode name="dn1" dataHost="dh1" database="testdb" /> <dataHost name="dh1" maxCon="500" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1"> <heartbeat>select user()</heartbeat> <writeHost host="m1" url="192.168.56.101:3306" user="mycat" password="YourPass123"> <readHost host="s1" url="192.168.56.102:3306" user="mycat" password="YourPass123" weight="1"/> </writeHost> </dataHost> </mycat:schema>

逐个属性解释清楚:

schema的checkSQLschema="false",意思是不要让Mycat自动给SQL加库名前缀。加了反而容易在跨库查询时出问题。sqlMaxLimit="100",表示如果SQL没有写limit,Mycat会补一个limit 100,防止一次查询把后端拖死。这不是让你偷懒不写limit,而是给那些忘了写limit的请求兜底。

dataNode的database="testdb",对应的是后端MySQL里真实存在的库名。dataHost里的maxCon和minCon是Mycat到后端MySQL的连接池参数,maxCon设多大,要和后端MySQL的max_connections配合考虑,这个坑我后面第五节会专门说。

最关键的是dataHost上的balance="1",配合writeHost里嵌套的readHost,就实现了完整读写分离:写请求强制去WriteHost,读请求全部去readHost指向的从库。

heartbeat是Mycat检测后端节点存活的探活SQL,select user()开销极小,不会给数据库增加负担。writeHost和readHost里配的user和password,就是我们在主库上创建的mycat账号。这里最容易犯的错是只给主库配了账号、忘记在从库上建同样的账号,结果读写分离一开,读请求全部报权限错误。

4.4 只做读写分离,要不要配分片规则

很多朋友一听到Mycat就觉得必须配rule.xml,其实不是。如果当前只是想让主从库分担读写压力,完全可以不碰分片规则,schema里直接用dataNode指定数据落到哪个节点就行。rule.xml是用来做分库分表的,当某张表的数据量大到一张物理表扛不住的时候才需要。

但我的建议是,配置schema.xml的时候留好分片的扩展位。就算现在不配rule.xml,也要在表结构设计上保证分片键存在,比如订单表的用户ID字段。否则将来要上分片,改表结构比现在改配置痛苦得多。

4.5 启动Mycat并测试链路

先前台启动看日志最稳妥:

bin/mycat console

看到启动成功后,再后台启动:

bin/mycat start

然后用MySQL客户端测试连通性:

mysql -h192.168.56.103 -P8066 -umycat -pMycat123

登录成功后执行:

SHOW DATABASES; USE testdb; SELECT * FROM t_user LIMIT 1;

到这里如果都能正常返回,说明Mycat这个代理层已经和后端MySQL打通了。下一步就要验证读写分离到底有没有生效。

5. 读写分离是否真的生效:三种验证手段

5.1 在Mycat侧观察分派结果

第一个办法是在Mycat上执行管理命令:

SHOW @@DATASOURCE;

这个命令会列出所有dataHost下writeHost和readHost的状态,能看到每个节点的连接数、心跳状态,至少能确认Mycat是否认为从库是活的。

再进一步:

SHOW @@SQL;

这里能看到最近执行过的SQL以及它们被分发到了哪个后端节点。只要select语句出现在从库那个节点的列表里,就说明读写分离逻辑在起作用。

5.2 在主从库两侧抓查询现场

比Mycat日志更直观的办法,是在MySQL侧抓现场。在从库执行:

SHOW FULL PROCESSLIST;

如果看到来自192.168.56.103的连接正在执行select,说明读流量确实到了从库。主库上应该只有写操作和一些管理连接,select的数量应该非常少。

我建议写个简单脚本循环跑100次select,同时盯着主库的processlist看select数量。如果主库上一堆查询排队,而配置里balance又是1,基本可以判断读写分离没生效,需要按第六部分的排查思路走下去。

5.3 故障演练:从库挂了会怎样

验证完正常路径,我建议做一个故障演练。

把从库MySQL停掉,再发读请求。在balance="1"的配置下,Mycat心跳会发现readHost离线,读请求会报错,而写请求不受影响。这个结果其实是符合预期的:主库在balance="1"语义下不承担读,从库挂了读自然不可用。

所以在生产环境,一主一从的架构是不够稳的。要么配两台从库,要么把balance改成2,让主库在从库故障时能兜底读流量。我见过不少团队上线读写分离之后栽在这个细节上,测试环境什么都好,从库一被大查询拖垮,整套系统的读接口全部雪崩。

再来一个更狠的演练:模拟主库宕机。如果switchType配置为1,Mycat会自动把写请求切到原从库上。但这里我必须重复一遍:自动切换本身不代表安全。切换之前必须确认主从数据的一致性,GTID或者日志位点的对齐是前提。这个切换策略需要在测试环境反复演练,每个团队的数据一致性要求不同,不能一概而论。

6. 实测中的常见坑与完整排查链路

6.1 读流量还是打在主库上,怎么排查

这是最让人头疼的问题。配置看着没问题,但主库的select就是源源不断。我一般按这个顺序排查:

第一,确认应用连接的端口。应用jdbc url写的还是3306,压根就没走Mycat 8066端口,这种错误最隐蔽,因为业务完全正常,只是读写分离没有参与进来。检查所有数据源配置,确保url指向Mycat所在机器的8066。

第二,检查应用是否开启了事务。Mycat对事务内查询有一个明确的策略:一旦进入事务,读写都绑定同一个后端连接,全部走主库。换句话说,你在一个事务里先update再select,这个select不会去从库。这是为了保证事务隔离级别和可重复读的行为,不是配置问题。所以排查的时候要看看那些"应该走从库却没走"的查询,是不是被包在事务里了。

第三,检查SQL是否带了强制主库的注解。Mycat支持类似/*!mycat:db_type=master*/这样的注解,一旦写了,这条SQL就明确要求走主库。有些框架或者数据访问层会默认加上这个注解保证强一致读,注意识别。

第四,在Mycat上执行SHOW @@SQL,看看这些select最终被分到哪个节点。如果分发结果全部指向writeHost,说明路由策略或者balance配置有误,再回schema.xml逐项排查。

6.2 从库复制中断:IO线程和SQL线程

从库如果Seconds_Behind_Master一直在涨,或者SHOW SLAVE STATUS里出现了No,读写分离的读数据立刻变得不可靠。常见原因有三类。

一是server-id冲突。多台实例的server-id必须唯一,这个字段有2的32次方个取值,只要建实例时用心规划,很少会撞。但一旦撞上,IO线程的表现就是反复断开重连。

二是binlog_format不一致。主库和从库如果binlog格式不同,在某些SQL回放时会报错。最典型的错误是statement格式下使用了不确定函数,造成主从数据不一致。我习惯统一用ROW格式,配合GTID,数据最稳。

三是从库表结构和主库不一致。比如某个表主库加了列,从库没同步这个DDL,后面所有的insert都会回放失败。处理办法是先停掉复制线程,在从库补齐表结构,再START SLAVE。

排查的时候,核心就是那一句话:

SHOW SLAVE STATUS\G

重点看Last_Error字段指向哪张表、哪个操作,根据错误去从库修数据。修完执行STOP SLAVE; START SLAVE;重新启动复制线程。

6.3 从库延迟与强一致读的取舍

读写分离绕不开的痛点是延迟。主库写进去一条数据,到从库能查到,中间通常有几十到几百毫秒的间隔。业务如果要求"写完立刻读到",这种一致性要求会导致读写分离失效,因为读请求必须走主库。

我的处理习惯是按业务分级。对一致性要求极高的场景,比如支付结果、库存扣减后的确认,读请求强制走主库,可以用Mycat注解或者单独配置一个访问主库的数据源。对允许秒级延迟的查询,比如文章列表、商品详情页,放心走从库。再往上,如果缓存层能挡住绝大部分重复读,从库延迟的影响还会进一步降低。整体原则是:不追求所有查询都读最新的数据,而是让每个查询都知道自己能容忍多旧的数据。

6.4 maxCon和MySQL连接数被同时打满

Mycat的maxCon是它到后端MySQL的最大连接池上限,而不是前端应用连接数。如果Mycat配了maxCon=500,而后端MySQL的max_connections只有200,那连接池很快就会被撑爆。慢查询越多,单个连接被占用的时间越长,池子越紧张。

我见过一个案例,某次上线后从库CPU一直很高,Mycat日志里报连接超时,检查下来是慢查询太多,每个查询要跑好几秒,后端连接全部被慢查询占住,新来的请求排不上队。解决思路有两个方向:一是优化慢查询,把大查询拆小,让连接快速释放;二是合理调大MySQL的max_connections,但同时要注意机器内存能不能撑住这么多线程的开销。连接数不是越大越好,建连接本身有内存和上下文切换成本。

排查这一步,直接看Mycat上的:

SHOW @@CONNECTION;

如果看到大量Sleep连接堆积,说明连接池里的空闲连接没被复用,再配合后端MySQL的processlist确认它们卡在哪里。

7. 从读写分离到后续演进

7.1 一主多从,读能力怎么横向扩展

读写分离跑稳之后,第一个自然的演进就是增加从库。业务读需求继续涨,一台从库不够了,那就再加一台。Mycat的配置方式很简单,在writeHost节点里再加一个readHost标签:

<writeHost host="m1" url="192.168.56.101:3306" user="mycat" password="YourPass123"> <readHost host="s1" url="192.168.56.102:3306" user="mycat" password="YourPass123" weight="1"/> <readHost host="s2" url="192.168.56.104:3306" user="mycat" password="YourPass123" weight="2"/> </writeHost>

weight字段控制权重,权重越高的节点收到的读流量越多。我建议先从weight一致开始,等通过对业务流量分布的观察再调整,不要一开始就人为制造不均衡。

7.2 分库分表和读写分离叠加

系统继续变大,单表数据过千万,读压力分散到好几台从库也架不住全表扫描。这个阶段就该上分片了。Mycat的做法是给特定表配置分片规则,同一张表的数据按分片键散落到多个库,每个库都有自己的主从节点。业务侧看到的还是同一个逻辑库,SQL的路由和读写分发都由Mycat完成。

叠加和单独使用的区别在于,每个分片的dataNode都要有自己独立的writeHost和readHost。配置量变大了,但架构思路没变,还是那套schema、dataNode、dataHost的映射关系。分片规则的制定是另一个大话题,这里只强调一点:分片键的选型要在业务初期就决定好,将来迁移的成本极高。

7.3 团队落地流程的一些个人建议

最后聊聊落地的节奏。按我自己的经验,最快也最稳妥的顺序是这样的:先在测试环境完整部署主从和Mycat,用压测工具打一轮高并发读,确认分派逻辑和延迟是否符合预期;然后让两三个内部低风险系统先切到Mycat跑一两个星期,观察中间件自身的稳定性;最后再接核心线上流量。

正式切换时,应用连接串的变更尽量放在低峰期灰度,并且准备好回滚方案。回滚很简单,把数据源指回主库地址就能恢复原状,但要预演一遍确认这个操作真的可行。整个过程最容易被忽略的是监控:Mycat要有存活监控,MySQL侧要有连接数、复制延迟、慢查询的看板,应用侧要能看到所有数据库请求的RT分布。两侧的监控数据结合,才能在出问题时快速定位到底是代理层、网络层还是数据库本身的问题。我个人的习惯是,所有配置变更都同时更新到配置管理平台并留存变更记录,这样每次出问题都能回溯"当时改了什么、为什么改"。

这个方案从单库到主从到读写分离,再到后面的分片叠加,链路每一步都有明确的验证方法。线上环境的数据规模、团队运维能力各不相同,但先把这套基础链路在自己掌控的环境里跑熟,比任何现成的架构方案都可靠。

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

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

立即咨询