线上MySQL读写分离,出现写完读不到问题如何解决

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 今天我们来详细了解一下主从同步延迟时读写分离发生写后读不到的问题,依次讲解问题出现的原因,解决策略以及 Sharding-jdbc、MyCat 和 MaxScale 等开源数据库中间件具体的实现方案。

大家好,我是历小冰。


今天我们来详细了解一下主从同步延迟时读写分离发生写后读不到的问题,依次讲解问题出现的原因,解决策略以及 Sharding-jdbc、MyCat 和 MaxScale 等开源数据库中间件具体的实现方案。

写后读不到问题

MySQL 经典的一主两从三节点架构是大多数创业公司初期使用的主流数据存储方案之一,主节点处理写操作,两个从节点处理读操作,分摊了主库的压力。


但是,有时候可能会遇到执行完写操作后,立刻去读发现读不到或者读到旧状态的尴尬场景。这是由于主从同步可能存在延迟,在主节点执行完写操作,再去从节点执行读操作,读取了之前旧的状态。

image.png


上图展示了此类问题出现的操作顺序示意图:

  • 客户端首先通过代理向主节点 Master 进行了写入操作
  • 紧接着第二步去从节点 Slave A 执行读操作,此时 Master 和 Slave A 之间的同步还未完成,所以第二步的读操作读取到了旧状态
  • 当第五步再次进行读操作时,此时同步已经完成,所以可以从 Slave B 中读取到正确的状态。

下面,我们就来看一下为什么会出现此类问题。

MySQL 主从同步

理解问题背后发生的原因,才能更好的解决问题。MySQL 主从复制的过程大致如下图所示,本篇文章只讲解同步过程中的流程,建立同步连接和失联重传不是重点,暂不讲解,感兴趣的同学可以自行了解。

image.png

MySQL 主从复制,涉及主从两个节点,一共四个四个线程参与其中:

  • 主节点的 Client Thread,处理客户端请求的线程,执行如图所示的1~5步骤,2,3,4步骤是为了保证数据的一致性和尽量减少丢失,第三步骤时会通知 Dump Thread;
  • 主节点的 Dump Thread,接收到 Client Thread 通知后,负责读取本地的 binlog 的数据,将 binlog 数据,binlog 文件名 以及当前发送 binlog 的位置信息发送给从节点;
  • 从节点的 IO Thread 负责接收 Dump Thread 发送的 binlog 数据和相关位置信息,将其追加到本地的 relay log 等文件中;
  • 从节点的 SQL Thread 检测到 relay log 追加了新数据,则解析其内容(其实就是解析 binlog 文件的内容)为可以执行的 SQL 语句,然后在本地数据执行,并记录下当前执行的 relay log 位置。


上述是默认的异步同步模式,我们发现,从主节点提交成功到从节点同步完成,中间间隔了6,7,8,9,10多个步骤,涉及到一次网络传输,多次文件读取和写入的磁盘 IO 操作,以及最后的 SQL 执行的 CPU 操作。


所以,当主从节点间网络传输出现问题,或者从节点性能较低时,主从节点间的同步就会出现延迟,导致文章一开始提及的写后读不到的问题。在高并发场景,从节点一般要过几十毫秒,甚至几百毫秒才能读到最新的状态。

常见的解决策略

一般来讲,大致有如下方案解决写后读不出问题:

  • 强制走主库
  • 判断主备无延迟
  • 等主库位点或 GTID 方案

强制走主库

强制走主库方案最容易理解和实现,它也是最常用的方案。顾名思义,它就是强制让部分必须要读到最新状态的读操作去主节点执行,这样就不会出现写后读不出问题。这种方案问题在于将一部分读压力给了主节点,部分破化了读写分离的目的,降低了整个系统的扩展性。


一般主流的数据库中间件都提供了强制走主库的机制,比如,在 sharding-jdbc 中,可以使用 Hint 来强制路由主库。

HintManagerhintManager=HintManager.getInstance();
hintManager.setMasterRouteOnly();
// 继续JDBC操作


它的原理就是在 SQL 语句前添加 Hint,然后数据库中间件会识别出 Hint,将其路由到主节点。


下面,我们就来看一下如果要去从库查询,并且要避免过期读的方案,并分析各个方案的优缺点。

判断主备无延迟

第二种方案是使用 show slave status 语句结果中的部分值来判断主从同步的延迟时间:

>showslavestatus***************************1.row***************************Master_Log_File: mysql-bin.001822Read_Master_Log_Pos: 290072815Seconds_Behind_Master: 2923Relay_Master_Log_File: mysql-bin.001821Exec_Master_Log_Pos: 256529431Auto_Position: 0Retrieved_Gtid_Set: 
Executed_Gtid_Set: 
.....


  • seconds_behind_master,表示落后主节点秒数,如果此值为0,则表示主从无延迟
  • Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点,Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点。如果这两组值相等,则表示主从无延迟
  • Auto_Position=1 ,表示使用了 GTID 协议,并且备库收到的所有日志的 GTID 集合 Retrieved_Gtid_Set 和 执行完成的 GTID 集合 Executed_Gtid_Set 相等,则表示主从无延迟。


在进行读操作前,先根据上述方式来判断主从是否有延迟,如果有延迟,则一直等待到无延迟后执行。但是这类方案在判断是否有延迟时存在着假阳和假阴的问题:

  • 判断无延迟,其他延迟了。因为上述判断是基于从节点的状态,当主节点的 Dump Thread 尚未将最新状态发送给从节点的 IO SQL 时,从节点可能会错误的判断自己和主节点无延迟。
  • 判断有延迟,但是读操作读取的最新状态已经同步。因为 MySQL 主从复制是一直在进行的,写后直接读的同时可能还有其他无关写操作,虽然主从有延迟,但是对于第一次写操作的同步已经完成,所以读操作已经可以读到最新的状态。


对于第一个问题,需要使用主从复制的 semi-sync 模式,上文中讲解介绍的是默认的异步模式,semi-sync 模式的流程如下图所示:


image.png


  • 当主节点事务提交的时候,Dump Thread 把 binlog 发给从节点;
  • 从节点的 IO Thread 收到 binlog 以后,发回给主节点一个 ack,表示收到了;
  • 主节点的 Dump Thread 收到这个 ack 以后,再通知 Client Thread ,此时才能给客户端返回执行成功的响应。


这样,写操作执行后,就确保从节点已经读取到主节点发送的 binglog 数据,即 Master_Log_File、 Read_Master_Log_Pos 或 Retrieved_Gtid_Set 是最新的,这样才能与执行的相关数据进行对比,判断是否有延迟。


可惜的是,上述 semi-sync 模式只需要等待一个从节点的ACK,所以一主多从的模式该方案将会无效。


虽然该方案有种种问题,但是对于一致性要求不那么高的场景也能适用,比如 MyCat 就是用 seconds_behind_master 是否落后主节点过多,如果超过一定阈值,就将其从有效从节点列表中删除,不再将读请求路由到它身上。


在 MyCAT 的用于监听从节点状态,发送心跳的 MySQLDetector 类中,它会读取从节点的 seconds_behind_master,如果其值大于配置的 slaveThreshold,则将打印日志,并将延迟时间设置到心跳信息中。

StringSeconds_Behind_Master=resultResult.get( "Seconds_Behind_Master");          
if (null==Seconds_Behind_Master ){
MySQLHeartbeat.LOGGER.warn("Master is down but its relay log is clean.");
heartbeat.setSlaveBehindMaster(0);
}elseif(!"".equals(Seconds_Behind_Master)) {
intBehind_Master=Integer.parseInt(Seconds_Behind_Master);
if ( Behind_Master>source.getHostConfig().getSlaveThreshold() ) {
MySQLHeartbeat.LOGGER.warn("found MySQL master/slave Replication delay !!! "+heartbeat.getSource().getConfig() +", binlog sync time delay: "+Behind_Master+"s" );
    }           
heartbeat.setSlaveBehindMaster( Behind_Master );
}


下面,我们就介绍能够解决第二个问题的方案,即判断有延迟,但是读操作读取的特定最新状态已经同步。

等GTID 方案

首先介绍一下 GTID,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。它由MySQL 实例的uuid和一个整数组成,该整数由该实例维护,初始值是 1,每次该实例提交事务后都会加一。


MySQL 提供了一条基于 GTID 的命令,用于在从节点上执行,等待从库同步到了对应的 GTID(binlog文件中会包含 GTID),或者超时返回。

selectwait_for_executed_gtid_set(gtid_set, timeout);


MySQL 在执行完事务后,会将该事务的 GTID 会给客户端,然后客户端可以使用该命令去要执行读操作的从库中执行,等待该 GTID,等待成功后,再执行读操作;如果等待超时,则去主库执行读操作,或者再换一个从库执行上述流程。


MariaDB 的 MaxScale 就是使用该方案,MaxScale 是 MariaDB 开发的一个数据库智能代理服务(也支持 MySQL),允许根据数据库 SQL 语句将请求转向目标一个到多个服务器,可设定各种复杂程度的转向规则。


image.png


MaxScale 在其 readwritesplit.hh 头文件和 rwsplit_causal_reads.cc 文件中的 add_prefix_wait_gtid 函数中使用了上述方案。

#define MYSQL_WAIT_GTID_FUNC   "WAIT_FOR_EXECUTED_GTID_SET"staticconstchargtid_wait_stmt[] ="SET @maxscale_secret_variable=(SELECT CASE WHEN %s('%s', %s) = 0 ""THEN 1 ELSE (SELECT 1 FROM INFORMATION_SCHEMA.ENGINES) END);";
GWBUF*RWSplitSession::add_prefix_wait_gtid(uint64_tversion, GWBUF*origin) {
    ....
snprintf(prefix_sql, prefix_len, gtid_wait_stmt, wait_func, gtid_position.c_str(),  gtid_wait_timeout);
    ....
}


举个例子,原来要执行读操作的 SQL 和添加了前缀的 SQL 如下所示:

SELECT*FROM `city`;SET @maxscale_secret_variable=(SELECT CASE WHEN WAIT_FOR_EXECUTED_GTID_SET('232-1-1',10)=0 THEN 1 ELSE (SELECT1FROM INFORMATION_SCHEMA.ENGINES) END);SELECT*FROM `city`;


当 WAIT_FOR_EXECUTED_GTID_SET 执行失败后,原 SQL 就不会再执行,而是将该 SQL 去主节点执行。

后记

感觉大家一直读到文末,后续小冰会继续为大家奉上高质量的文章,也希望大家继续关注。

个人博客,欢迎来玩


参考

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
负载均衡 监控 关系型数据库
MySQL 官宣:支持读写分离了!!
【10月更文挑战第8天】MySQL的读写分离功能显著提升了数据库性能、可用性和可靠性。通过将读写操作分配至不同服务器,有效减轻单个服务器负载,提高响应速度与吞吐量,并增强系统稳定性。此外,它还支持便捷的扩展方式,可通过增加只读服务器提升读操作性能。实现读写分离的方法包括软件层面(如使用数据库中间件)和硬件层面(使用独立服务器)。使用时需注意数据一致性、负载均衡及监控管理等问题。
133 0
|
7月前
|
SQL 关系型数据库 MySQL
mysql 主从复制与读写分离
mysql 主从复制与读写分离
|
3月前
|
关系型数据库 MySQL Java
MySQL主从复制实现读写分离
MySQL主从复制(二进制日志)、 Sharding-JDBC实现读写分离
MySQL主从复制实现读写分离
|
4月前
|
SQL 关系型数据库 MySQL
(二十五)MySQL主从实践篇:超详细版读写分离、双主热备架构搭建教学
在上篇《主从原理篇》中,基本上把主从复制原理、主从架构模式、数据同步方式、复制技术优化.....等各类细枝末节讲清楚了,本章则准备真正对聊到的几种主从模式落地实践,但实践的内容通常比较枯燥乏味,因为就是调整各种配置、设置各种参数等步骤。
618 3
|
4月前
|
关系型数据库 MySQL PHP
开发者必看:MySQL主从复制与Laravel读写分离的完美搭配
开发者必看:MySQL主从复制与Laravel读写分离的完美搭配
79 2
|
4月前
|
SQL 关系型数据库 MySQL
mysql读写分离,主从同步
本文介绍了如何在Laravel项目中配置数据库读写分离,并实现MySQL主从同步。主要步骤包括:在`config/database.php`中设置读写分离配置;为主机授予从机访问权限;配置各MySQL服务器的`/etc/my.cnf`文件以确保唯一的`server-id`;以及通过SQL命令设置主从关系并启动从服务。文章还针对一些常见错误提供了排查方法。最后通过验证确认主从同步是否成功。[原文链接](https://juejin.cn/post/6901581801458958344)。版权所有者为作者佤邦帮主,转载请遵循相关规定。
|
4月前
|
cobar 关系型数据库 MySQL
使用MyCat实现MySQL主从读写分离(一)概述
【8月更文挑战第11天】MySQL读写分离通过主从复制分散负载,主库负责写操作,从库承担读查询,以复制技术确保数据一致性。此策略有效缓解锁竞争,提升查询效能并增强系统可用性。实现方式包括应用层处理,简便快捷但灵活性受限;或采用中间件如MyCAT、Vitess等,支持复杂场景但需专业团队维护。
132 0
|
4月前
|
SQL 关系型数据库 MySQL
基于proxysql实现MySQL读写分离
基于proxysql实现MySQL读写分离
|
6月前
|
关系型数据库 MySQL
mysql-proxy实现mysql的读写分离
mysql-proxy实现mysql的读写分离
|
6月前
|
分布式计算 DataWorks 关系型数据库
DataWorks操作报错合集之在数据集成到MySQL时,遇到特殊字符导致的脏数据如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。