ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL 8.2发布≠支持读写分离,一文读懂读写分离架构与落地

MySQL 8.2发布≠支持读写分离,一文读懂读写分离架构与落地

看到“牛掰,MySQL 8.2 支持读写分离了!”这个标题的时候,我第一反应也是赶紧点进去看看,心想MySQL官方终于开窍了,准备把读写分离这种架构级功能塞进内核里了?结果看完内容才发现,这标题起得真是让人一口气差点没上来——MySQL 8.2 确实发布了,也确实有点新东西,但“读写分离”这个能力,跟数据库大版本号还真没有半毛钱直接关系。

先别急着喷我,我理解大家看到这种标题会兴奋。毕竟读写分离是MySQL高并发架构里最常被提到的一招,如果能被官方直接内置,那我们这帮DBA和开发就不用折腾什么中间件、代理层了。但实际情况是,读写分离从来不是一个数据库功能,而是一种架构设计。今天我就借着这个标题,把读写分离这点事从头到尾掰扯清楚,包括它到底怎么实现、为什么MySQL自己不干这活、以及如果你真想在生产环境搞一套,应该怎么落地。

1. 先别急着嗨:MySQL 8.2 到底更新了什么

1.1 8.2 是创新版,不是长期支持版

先说版本。MySQL 8.2是Oracle在2023年10月推出的一个Innovation Release,注意这个词,“创新版本”。它跟8.0系列的LTS(长期支持)版本走的不是一条路。简单理解,就是Oracle把MySQL发版节奏改了,以后每季度出一个创新版,里面放一些新特性和性能优化,但只支持到下一个创新版出来之前,跨度也就几个月。真正需要稳定运维的人,还是会选择8.0.3x这种LTS版本继续用。

这个变化本身其实比“读写分离”更有意思,因为它意味着MySQL开始走快速迭代路线了。但快速迭代不等于所有功能都往内核里塞,尤其是读写分离这种跟业务强相关的东西,官方始终很克制。

1.2 8.2 真正值得关注的新特性

我翻了翻8.2的Release Notes,印象比较深的有几个点,但都很“内功”。比如InnoDB在崩溃恢复上的优化,还有一些SQL解析和优化器层面的调整,都是那种“你感知不到但线上跑着跑着就快了一点”的改进。另外8.2对Redundant行格式进行了废弃警告,以后可能彻底移除。这些都是正经事,但跟“读写分离”没有半点关系。

网上一些自媒体把8.2包装成“官方支持读写分离”,我猜测背后可能是看到某个版本更新里提到了“只读实例”“副本”之类的词就嗨了。但这些东西在MySQL里早就有,跟8.2没关系。所以标题里的“牛掰”应该改成“MySQL 8.2 更新了,但读写分离还是得自己搞”才准确。

2. 读写分离到底是什么,为什么数据库内核不直接“支持”

2.1 读写分离的本质:一主多从,读写分流

我们先对齐一下概念。所谓读写分离,就是把写操作(INSERT、UPDATE、DELETE)发到主库(Master或Primary),把读操作(SELECT)发到从库(Slave或Replica)。一般是一个主库挂多个从库,从库通过主从复制同步数据。

这种架构能扛住“读多写少”的场景。比如一个电商网站,用户浏览商品、查订单、看库存,都是读操作,可能占了九成。如果所有流量都打到一个库上,很快CPU和IO就顶不住了。但如果你拖了三个从库,把读流量分散到三个机器上,主库就只需要扛写入那一小部分压力,整体并发能力和吞吐量直接翻几倍。

在真正动手之前,你得理解一个核心点:主从复制是数据同步机制,读写分离是路由机制。复制解决的是“每个库上有什么数据”,路由解决的是“每个请求去哪个库执行”。两者分开,但必须配合。

2.2 为什么MySQL内核不做这件事

这是很多人的疑问。既然读写分离这么有用,为什么MySQL不直接把路由逻辑内置了?我刚开始也觉得官方有点迟钝,但后来想通了,因为这事不好干,而且也不该数据库来干。

关键在于“读”和“写”的语义很难自动判断。你说SELECT是读,那SELECT ... FOR UPDATE呢?它虽然是SELECT,但带了写锁,必须在主库执行。还有事务里的读,为了保证事务隔离性,必须整条事务都发到同一个库上,不能第一条语句走主库,第二条语句走从库,否则你在事务里刚刚插入的数据,回头一查没了,因为复制还没同步过去。

这还只是最简单的情况。如果再加点业务规则,比如某些大客户的数据必须读主库,某些报表查询只需要读一台特定的从库,甚至需要根据SQL类型做更细粒度的路由。这种规则完全跟业务绑定,不可能靠数据库内核去猜。所以正确做法是让一个中间层做代理,要么是独立的中间件,要么是应用层自己实现路由逻辑。

2.3 数据库版本与读写分离的关系

明确了上面这点,你就明白读写分离跟MySQL版本真没关系。无论是MySQL 5.7还是8.0还是8.2,只要你开了binlog、配好了主从复制,外部加一层路由就能实现读写分离。版本只影响复制机制本身,比如8.0之后复制是基于GTID的,比基于日志文件和位置的旧方式方便很多。但这不是“新版本支持了读写分离”,而是新版本让搭建复制更简单了。

我现在的生产环境还在跑8.0.34,也没升到8.2,读写分离一样用得飞起。所以别被标题坑了,版本不是关键,架构才是。

3. 手把手搭建一主一从 + 读写分离(基于实际实践)

下面这部分是实打实的干货,我会用一套我自己搭过的环境做示例。主库和从库都是MySQL,一台写,一台读,然后中间用代理层做路由。为了更贴近8.2的标题,我顺便也提一句,这套玩法在8.2上没有任何额外配置,完全通用。

3.1 环境准备

我这边模拟环境是两台服务器,这里叫它们db-master和db-replica。操作系统都是Ubuntu 22.04,MySQL版本我用的是8.0.33,因为手头这台稳定的机器就是它,你要用8.2也完全一样。另外还有一台装了代理服务,后面讲。

先做几项基础准备,两台机器上都要改MySQL配置:

  • 主库和从库都要使用独立的server_id。
  • 主库需要开启log_bin。
  • 从库需要配置relay_log。
  • 保证网络可以互通,主库的3306端口要对从库和代理开放。

MySQL的配置文件一般放在/etc/mysql/mysql.conf.d/mysqld.cnf,我习惯把自定义配置单独放在/etc/mysql/conf.d/下,比如repl.cnf,方便维护。

3.2 配置主库

主库db-master上,我新建了一个配置文件,内容如下:

[mysqld] server_id = 100 log_bin = mysql-bin binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON

这些参数的含义:

  • server_id:整个复制拓扑中唯一,我用100代表主库。
  • log_bin:开启二进制日志,主从复制的数据源全靠它。
  • binlog_format = ROW:行级复制,比STATEMENT更安全,避免函数、存储过程在主从执行结果不一致。
  • gtid_mode = ON和enforce_gtid_consistency = ON:开启GTID复制。这是8.0以后强烈推荐的做法,排障和切换比老式的文件名+偏移量方式方便太多。

改完配置重启MySQL,然后创建一个用于复制的账号:

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

这里注意,MySQL 8.0默认的认证插件是caching_sha2_password,如果你的从库版本也是8.0,那没问题。如果是5.7或更老的版本,需要在这里指定mysql_native_password,否则从库连不上。这也是我在实践中踩过的坑,后面会展开讲。

3.3 配置从库

在db-replica上,配置文件如下:

[mysqld] server_id = 101 relay_log = mysql-relay-bin read_only = ON gtid_mode = ON enforce_gtid_consistency = ON

这里read_only = ON非常关键。它让从库只接收带有特权通道的写入,防止应用或者某个误操作直接对从库写数据。但要注意,read_only只是对普通用户生效,超级权限账号还是能写,所以还得靠权限管理和应用层自觉。

重启从库MySQL后,执行复制指令:

CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.1.100', SOURCE_USER='repl', SOURCE_PASSWORD='RepL@123456', SOURCE_AUTO_POSITION=1; START REPLICA;

注意,8.0.23之前用的是CHANGE MASTER TO,之后改成了CHANGE REPLICATION SOURCE TO,但老命令仍然兼容。我用的是新写法,避免未来被废弃。

启动后赶紧看一眼复制状态:

SHOW REPLICA STATUS\G

重点关注两个线程:

  • Replica_IO_Running: Yes
  • Replica_SQL_Running: Yes

还有Seconds_Behind_Source: 0,表示没有延迟。如果这里有报错,先看Last_IO_Error和Last_SQL_Error,大部分问题都能从这两个字段找到线索。

3.4 用代理层实现读写分离

主从复制配好之后,现在只解决了数据同步的问题,还没有实现“读写分离”。读写分离要让应用连到一个统一入口,由入口判断该把SQL发给谁。这里有很多选择,我用的比较多的是MySQL Router,因为它是官方出的,轻量,配置也不复杂。

在代理服务器上安装MySQL Router,比如用apt或者直接下载二进制包。8.2版本的Router和MySQL Server的版本号是对应的,我这次用的是router 8.2,正好呼应标题。

不过说实话,Router 8.2和8.0的核心功能没有本质变化,读写分离的路由策略还是那几样。我用它的原因是官方支持好,而且配置特别直观。创建一个配置文件/etc/mysqlrouter/mysqlrouter.conf,内容如下:

[DEFAULT] logging_folder=/var/log/mysqlrouter [metadata_cache:mycluster] router_mode=read-write cluster_name=mycluster user=mysqlrouter [routing:primary_rw] bind_address=0.0.0.0:6446 destinations=192.168.1.100:3306 routing_strategy=first-available protocol=classic [routing:secondary_ro] bind_address=0.0.0.0:6447 destinations=192.168.1.101:3306 routing_strategy=round-robin protocol=classic

这里我定义了两套路由规则:

  • 端口6446走主库,策略first-available,所有写和需要走主库的读都走这个端口。
  • 端口6447走从库,策略round-robin,普通读走这个端口,如果有多个从库会自动轮询。

Router启动后,应用层只需要把读写请求分别连到两个端口就行。比如配置数据库连接池时,写库连接串指向192.168.1.102:6446,读库连接串指向192.168.1.102:6447。Router负责把请求转发到真正的数据库。

如果你不想用Router,也可以用ProxySQL,它的功能更强,还能根据正则表达式对SQL做精细路由。但ProxySQL过于强大也有问题,就是配置复杂,出问题的时候排查成本高。我个人的建议是:如果你的团队没有专职DBA,先用Router就好;等以后架构复杂了,再上ProxySQL不迟。

3.5 验证读写分离是否真的生效

配置完了得验证一下,别信口开河。我习惯按照下面的方式确认:

  1. 在主库执行CREATE DATABASE test_rw;和CREATE TABLE test_rw.t(id INT PRIMARY KEY, c VARCHAR(50));。
  2. 确认从库已经同步出这两个库和表。
  3. 往主库写入一条数据:INSERT INTO test_rw.t VALUES (1, 'hello');。
  4. 在从库上直接查询,应该能看到这条数据,因为复制会把它带过去。
  5. 然后在应用层分别通过两个端口查询验证:通过6446查询走主库,通过6447查询走从库。

为了确认路由确实按预期走了,我还会在从库上执行SHOW PROCESSLIST;,看有没有来自Router的查询连接。只要在6447端口执行查询时从库的Command列出现Query,并且在主库上没有对应的查询,就说明读流量被正确分配到了从库。

这套流程跑通了,读写分离才算真正落地。

4. 踩过的一些坑:读写分离的常见问题与排查

读写分离看着简单,真正跑起来以后,各种坑一个接一个。下面这些问题都是我实际遇到过、并且花了不少时间排查的,列出来给大家做个参考。

4.1 主从延迟:刚写完就读不到

最典型的问题:用户在页面上提交了一个订单,然后立刻跳转到订单详情页,结果发现订单“不见了”。原因就是订单详情页走的读操作被路由到了从库,而主从复制还没把刚插入的数据同步过去。

这个问题在业务上非常严重,尤其是涉及钱、库存、状态变更的。解决思路有几种:

  • 对一致性要求极高的关键数据,强制走主库。比如根据用户ID取模,某些用户的所有请求都走主库;或者在代码里对特定查询标注“强制主库”。
  • 允许延迟存在,比如商品浏览、非关键列表的读延迟几百毫秒完全可接受。
  • 如果延迟实在太大,优先查从库的主从复制状态,看Seconds_Behind_Source,判断是网络问题还是从库执行大事务卡住了。

我在实践中的经验是:不要让中间件做“智能判断”来区分关键读和普通读,因为业务语义太复杂,很容易误判。最好在应用层通过数据源分类,比如一个叫masterDataSource,一个叫slaveDataSource,关键的读方法强制走master,普通查询走slave。明明白白,最安全。

4.2 从库数据不一致:复制中断怎么办

某天你发现从库数据不对,或者主从复制已经悄悄停了。这时候别慌,先执行SHOW REPLICA STATUS\G,看这两行:

  • Last_IO_Error
  • Last_SQL_Error

我遇到最多的是Last_SQL_Error,常见原因是主库执行了一个破坏性操作,比如在从库上人为改数据导致后续复制执行报错。或者某个DDL在从库执行失败,比如主库建了一张表,从库已经存在同名表。

处理办法通常是跳过错误。如果是一两条错误,可以临时设置:

SET GLOBAL sql_slave_skip_counter = 1; START REPLICA;

但注意,sql_slave_skip_counter在GTID模式下已经不起作用了。GTID模式下的跳错要复杂一些,一般是通过注入空事务的方式:

STOP REPLICA; SET GTID_NEXT = '自动获取失败的事务GTID'; BEGIN; COMMIT; SET GTID_NEXT = 'AUTOMATIC'; START REPLICA;

这里我建议各位不要一遇到复制报错就急着跳,先看看报错内容。如果错误是因为数据冲突,跳完以后也不会自动修复,后续还是会对不上。正确做法是比对主从数据,从备份中恢复一张表,或者干脆重新搭建从库。我现在的原则是:一旦确认复制中断且无法快速定位原因,直接重建从库,花不了多少时间,比在那瞎跳错误稳妥得多。

4.3 事务中的读写分离陷阱

这个坑特别隐蔽,也是我在代码评审时重点盯的。虽然你在应用层配置了读写分离,但像Spring这类框架,默认的事务边界是方法级别的。一个方法里既有SELECT又有UPDATE,如果框架根据“是否有写操作”来决定是否开启事务,然后在事务内把连接绑定到主库,那就没问题。最怕的是手写了一个查询方法,自己声明了@Transactional(readOnly = true),结果内部又调用了写操作,或者一个原本非事务的读方法,被某个切面增强后意外地开了事务。

事务一旦开启,连接就必须固定在一个库上,尤其要固定在主库上,因为事务内的读必须能看到事务内自己写的数据。如果你让事务内的读跑到从库,那大概率出现幻觉,也就是我们常说的“新读不旧”问题。

我的经验:在配置读写分离时,中间件一般会提供事务识别功能。比如Router会根据是否开启事务来决定路由。你在代码层面也得配合好,把事务的边界严格控制住,尽量让写操作的事务短小精悍,别在事务里做大量耗时查询,一方面会把主库拖垮,另一方面也失去读写分离的意义。

4.4 连接池配置不当导致连接打满

用了读写分离自然要配连接池,否则每条SQL都新建连接,性能还不如不搞。但连接池有个经典问题:如果读操作和写操作共用一个连接池,或者两个连接池总大小设置得一样,很容易出现某一边连接耗尽。

我遇到过的情况是,一次大促活动并发读特别高,从库连接池被占满,结果写操作的连接池也被后续的排队请求占满,整个系统假死。后来给读写两个连接池分别设定最大连接数,比如写库连接池最大50,读库连接池最大200,并且给读库连接池单独设置一个较短的超时时间。这样即使读压力爆了,也不会拖垮写路径。

另外注意连接池里的“空闲连接”,比如有些框架默认连接空闲超过一定时间就自动关闭,但如果MySQL的wait_timeout很短,中间件和连接池之间的连接会被数据库主动断开,导致池里全是无效连接。这个问题在MySQL 8.0上尤其明显,因为默认的wait_timeout是8小时,但某些云环境会调得很短。我现在的做法是把连接池的验证查询打开(比如SELECT 1),确保拿到手的连接都是活的。

4.5 从库挂了怎么办

无论你配置得多完美,从库总有挂的风险。挂一台从库,读流量就得重新分配,否则应用会报连接超时。我用的Router策略是round-robin,它本身不具备健康检查能力,或者说它默认只把路由发到“可达”的节点。如果某个从库宕机了,Router可能还会继续往那个IP发连接,导致部分请求失败。

规避方式很简单:别省额外的监控。我现在自己写了一个小脚本,每30秒探测一次从库的3306端口和MySQL响应,如果异常,就自动把Router配置文件里的这个从库移除,然后reload。如果从库数量很少,你甚至可以考虑把读路由统一收敛到一个高可用的负载均衡器后面,由负载均衡器来做健康检查。

在架构层面,更稳妥的是做成双主或主从半同步。8.2的复制机制本身也有增强,但这些话题展开就太长了。核心一点:读写分离的整体可用性,取决于你最弱的那个节点,别让从库成了整个系统的最短板。

5. 别让标题带偏了你:关于 MySQL 8.2 和读写分离的一些实话

最后聊点实实在在的感受。

MySQL 8.2这个版本,在我看来最大的价值是它延续了快速迭代、持续优化的信号,而不是说它一口气把架构级的能力都想明白了。读写分离这种功能,涉及业务路由、一致性语义、多副本管理,每一层都有大量细节,让数据库内核去统一解决,反而会限制使用场景的灵活性。

我见过很多团队,一看到“支持读写分离”这种标题就觉得是银弹,急着去升级数据库版本,结果升完发现该配的主从复制还是得配,该上中间件还是得上,甚至因为版本变更引入一些兼容性问题。所以我给所有人的建议是:架构问题首先要从架构层面解决,不要指望一个版本号帮你搞定所有事。

从实践角度,如果你手里有一个读多写少的业务,照着上面一主一从加Router的方式搭一套,大概率能扛住初期的高并发读压力。等业务做大了,再考虑从库扩容、分库分表、多级缓存这些后续手段。这个过程中,最关键的是搞清楚你的读路径和写路径分别有哪些业务约束,把它们列出来,再决定哪些读可以走从库、哪些必须强一致走主库。

我个人现在对读写分离的态度是:能用,但别滥用。每个从库都有复制成本,内存、磁盘、网络都有消耗,从库太多也会拖垮主库的binlog推送能力。结合业务实际,不一定非要搞三个从库,也许一主一从加上缓存就已经足够。这些判断,都得靠你对业务的理解,而不是看着哪个版本的发布说明就能拍板的。

最后分享一个验证小技巧:搭好读写分离后,用主库上的SHOW GLOBAL STATUS LIKE 'Com_%'和从库上的对应状态对比一下读写比例,你很快就能看到分离效果。如果从库上的Com_select数字蹭蹭往上涨,说明你的读流量真的被分流过去了。这个数字是很直观的成就感,比我上次看到“MySQL 8.2支持读写分离”这种标题要真实得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进