当前位置: 首页 > 科技观察

MySQL读写分离,写入后无法读取怎么解决

时间:2023-03-12 10:28:41 科技观察

本文转载自微信公众号《程序员李小兵》,作者李小兵。转载本文请联系程序员李小兵公众号。大家好,我是李小兵。今天我们就来详细了解一下主从同步延迟时读写分离的问题,然后讲解问题产生的原因,解决策略,以及Sharding等开源数据库中间件的具体实现方案-jdbc、MyCat和MaxScale。写完MySQL经典的一主二从三节点架构后无法读取的问题,是大多数创业公司早期采用的主流数据存储方案之一。主节点处理写操作,两个从节点处理读操作,分担主数据库压力。但是,有时候你可能会遇到尴尬的场景,执行完写操作后立即读取,发现无法读取或读取到旧状态。这是因为主从同步可能存在延迟。主节点执行写操作后,再去从节点执行读操作,读取之前的旧状态。上图为此类问题的操作顺序示意图:?客户端先通过代理向主节点Master写入。然后第二步去从节点SlaveA进行读操作。此时master和SlaveA之间的同步还没有完成,所以第二步的读操作读取的是oldstate。当第五步再次执行读操作时,此时同步已经完成,所以可以从SlaveB读取到正确的状态。接下来我们来看看为什么会出现这样的问题。MySQL主从同步了解问题背后的原因,才能更好的解决问题。MySQL主从复制的过程大致如下图所示。本文只讲解同步过程中的过程。同步连接的建立和丢失连接的重传不是重点。我暂时不解释。有兴趣的同学可以自行了解。MySQL主从复制涉及到两个主从节点,一共有四个四个线程参与其中:主节点的ClientThread,处理客户端请求的线程,执行步骤1~5如图,2,3,4第一步是保证数据的一致性,尽量减少丢失。第三步,通知DumpThread;master节点的DumpThread收到ClientThread的通知后,负责读取本地binlog的数据,并保存binlog数据和binlog文件。将当前发送的binlog的名称和位置信息发送给从节点;从节点的IOThread负责接收DumpThread发送过来的binlog数据和相关位置信息,并追加到本地relaylog等文件中;从节点的SQLThread检测到relaylog中有新的数据加入时,将其内容(其实就是binlog文件的内容)解析成可执行的SQL语句,然后在本地数据上执行,并把当前执行的中继日志位置被记录。以上是默认的异步同步方式。我们发现,从主节点提交成功到从节点同步完成,中间有6、7、8、9、10多个步骤,涉及一次网络传输、多次文件读取和磁盘IO操作用于写入,CPU操作用于最终的SQL执行。因此,当主从节点之间的网络传输出现问题,或者从节点性能低下时,主从节点之间的同步就会延迟,导致无法同步的问题文章开头提到的先写后读。在高并发场景下,从节点读取最新状态一般需要几十毫秒,甚至上百毫秒。常见的解决方案一般来说,解决写入后无法读取的问题,大致有以下几种解决方案:?强行移除主库?确定主备没有延迟?等待主库的位置libraryortheGTIDschemetoforcethemainlibrarytogo易于理解和实施,也是最常用的解决方案。顾名思义就是强制一些必须读到最新状态的读操作在master节点上执行,这样就不会出现先写后读失败的问题。该方案的问题是部分读压力交给了主节点,部分违背了读写分离的目的,降低了整个系统的可扩展性。一般主流的数据库中间件都会提供强制主库路由的机制。比如在sharding-jdbc中,可以通过Hint来强制主库进行路由。它的原理是在SQL语句之前添加Hint,然后数据库中间件会识别Hint并将其路由到master节点。下面我们来看看从数据库查询和避免过期读的方案,分析一下各个方案的优缺点。方案二是利用showslavestatus语句结果中的一些值来判断主从同步的延迟时间:>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,表示落后master节点的秒数,如果这个值为0,表示master-slave没有延时?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,表示主从没有延迟。延迟。在执行读操作之前,先按照上面的方法判断主从是否有延时,如果有延时,等到没有延时再执行。但是这类方案在判断是否有延迟时存在误报和漏报的问题:?判断没有延迟,但有其他延迟。因为上面的判断是基于slave节点的状态,所以当master节点的DumpThread还没有将最新的状态发送给slave节点的IOSQL时,slave节点可能会错误判断自己之间没有延时和主节点。?判断有延迟,但是读操作读到的最新状态已经同步。因为MySQL主从复制一直在进行,写完直接读的时候可能还有其他不相关的写操作。主从虽然有延迟,但是第一次写操作的同步已经完成,所以读操作已经可以读取到最新状态。第一个问题,需要用到主从复制的半同步模式。上面的解释是默认的异步模式。半同步模式的流程如下图所示:当主节点事务提交后,DumpThread将binlog发送给从节点;?从节点的IOThread收到binlog后,发回一个ack给master节点,表示已经收到;?master节点的DumpThread收到ack后,通知ClientThread,然后可以给client返回一个成功的响应。这样在写操作执行后,保证从节点读取了主节点发送的binglog数据,即Master_Log_File、Read_Master_Log_Pos或Retrieved_Gtid_Set是最新的,从而可以与相关执行数据进行对比,判断是否存在延迟。遗憾的是,上面的semi-sync模式只需要等待一个slave节点的ACK,这样一主多从的方案就会失效。这种方案虽然存在各种问题,但也可以应用于对一致性要求不高的场景。例如MyCat使用seconds_behind_master来检查master节点是否滞后过多。如果超过一定阈值,则将其从有效从节点列表中删除。然后将读取请求路由到它。在MyCAT的用于监控从节点状态和发送心跳的MySQLDetector类中,会读取从节点的seconds_behind_master。如果它的值大于配置的slaveThreshold,就会打印日志,并设置延迟时间到心跳信息。下面,我们介绍一种方案,可以解决第二个问题,即判断有延迟,但是读操作读到的具体最新状态已经同步。GTID方案首先引入GTID,即全局事务ID,在事务提交时产生,是事务的唯一标识。它由MySQL实例的uuid和一个整数组成,由实例维护。初始值为1,实例每提交一次事务就会加1。MySQL提供了一个基于GTID的命令在从节点上执行,等待从库同步到对应的GTID(binlog文件会包含GTID),或者超时后返回。MySQL执行完事务后,会把事务的GTID给客户端,然后客户端就可以使用这条命令执行从库的读操作,等待GTID,等待成功后再执行读操作;如果等待超时后,去主库执行读操作,或者换到另一个从库执行上述过程。MariaDB的MaxScale使用这个解决方案。MaxScale是MariaDB(也支持MySQL)开发的一种数据库智能代理服务,可以根据数据库SQL语句将请求重定向到一台或多台服务器,并可以设置各种复杂度的转向规则。MaxScale在其readwritesplit.hh头文件和rwsplit_causal_reads.cc文件中的add_prefix_wait_gtid函数中使用上述方案。例如要读取的原始SQL和带前缀的SQL如下:当WAIT_FOR_EXECUTED_GTID_SET执行失败时,不会执行原始SQL,而是在master节点上执行SQL。