如何避免 MYSQL 主从延迟带来的读写问题?
在 MYSQL 部署架构选型上,许多公司都会用到主从读写分离的架构,如下是一个一主一从的架构,主库 master 负责写入,从库 slave 进行读取。
但是既然是读写分离,必然会面临这样一个问题,当在主库上进行更新后,有可能数据还没来得及同步到从库,但是这个时候又有读数据的需求,为了能正确读取出数据,这个时候就只有读主库了。但是这样做增加了主库的压力,违反了我们做读写分离的初衷。所以这一节我们就来针对这种情况探讨下,如何尽量的避免对主库的压力,尽量的从从库读取数据。
主从复制的原理
在探讨解决方案前,我们先要对主从复制的原理有所了解,数据库的操作都会记录到 binlog,如下图所示,
1,从数据库(slave
)会启动两个线程io_thread
和sql_thread
,通过 io_thread 将自身与主数据库(master
)建立连接。
2,slave 向 master 发出要同步的位置信息(包含同步的文件名和偏移量),表示需要从该位置发起同步。
3,主数据库 master 将位置点后的 binlog 发送给 slave, slave 获取到本地形成relay log
(中转日志)。
4, 接着通过 sql_thread 解析 relay log,执行 sql。
从主从复制的过程可以看出,主从延迟时间是 在主库 master 执行 sql 的时间点到从库通过解析 relay log 执行 sql 后的时间点之间的差值。如果应用程序能够在 master 写入数据后等待这么一段时间,再去 slave 读取,就能正确的读取出来数据了。
但是这个时间差值是不确定的,究竟应用程序需要等待多久才去读取 slave,就成了我们需要思考🤔的问题。
如何避免延迟期间的主从数据不一致
比起在写入数据后读取主库或者写入数据后 sleep 一段时间读取从库,我给出两个我觉得比较靠谱点的方法。
判断位点是否同步
第一种方法是通过等待 slave 将 master 写入数据后的 binlog 的位点同步完成再对 slave 进行读取。
每次修改型 sql 的执行会将 master 的 binlog 的位点(日志偏移量)前移,如果在修改型 sql 执行完成后,能够获取到 master 的 binlog 位点,并且在客户端阻塞等待 slave 同步该位点完毕,再从 slave 读取就可以了。
MYSQL 中提供了一个函数select master_pos_wait(file, pos[, timeout])
用于在 slave 上执行等待 master 节点上的位点同步完成,其中 file,和 pos 是在 master 上的文件和位点,timeout 为了让master_pos_wait
函数在 timeout 秒内没有返回,则会直接触发超时返回。
返回结果解析,
返回结果正常情况下是一个大于 0 的整数,表示从 pos 位点开始完成了多少个事务。
如果直接返回结果 0,则说明在执行
select master_pos_wait(file, pos[, timeout])
时,位点已经同步完成。如果触发超时则返回-1。
如果执行期间 slave 发生错误,则返回 NULL。
所以,在判断是否应该在写入数据后读从库的逻辑,我们可以这样来写,
1, 在 master 写入数据后立马执行 show master status
,可以获取如下结果
可以看到 master 的 binlog 文件名称以及位点。
2, 在 slave 上执行 select master_pos_wait('mysql-bin.232011',3129472,1);
,如果 1s 内没有返回,则直接返回-1。
3, 在上一步如果触发超时返回返回-1,则直接读取主库,如果是>=0 的值,则直接读取从库。
这样便能最大程度从从库读取数据。
判断 GTID 是否同步
接着,我们来看下第二种方式,其实第二种方式和通过位点的方式类似,不同的是 slave 判断是否将数据同步完成的依据是看 GTID 的值。
什么是 GTID 值?
GTID 的全称是 Global Transaction Identifier
,全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。
MYSQL 开启 GTID 模式的方式是 在启动一个 MySQL 实例的时候,加上参数 gtid_mode=on
和 enforce_gtid_consistency=on
。
每个事务是和 GTID 值一一对应的,每个 MYSQL 实例会维护一个 GTID 集合,来表示实例执行过的事务。
在 slave 节点上,通过show slave status
可以看到 GTID 集合,如下图所示,
Auto_Position=1
,表示这对主备关系使用了 GTID 协议。Retrieved_Gtid_Set
,是备库收到的所有日志的 GTID 集合。Executed_Gtid_Set
,是备库所有已经执行完成的 GTID 集合。
如果 Executed_Gtid_Set 等于 Retrieved_Gtid_Set 说明 slave 将从 master 那里获取到的 binlog 全部执行完毕。
在 master 节点执行 show master status
,也能看到 GTID 集合,Executed_Gtid_Set
为 master 节点执行过的 GTID 集合。如下图所示,
GTID 模式下判断同步的步骤
在 GTID 模式下,从库 slave 从主库 master 取 binlog 的逻辑将不再是直接告诉 master 要取的文件和位点了,而是由 slave 将自身的 GTID 集合告诉 master。
master 再结合自身的 GTID 集合,找出在 master 中有但是在 slave 中没有的 GTID 集合,然后从 binlog 中找到第一个不在 GTID 集合中的事务,从该事务的 binlog 位点开始,往后读取 binlog 发送给 slave。
MYSQL 针对于 GTID 同样提供 了一个函数select wait_for_executed_gtid_set(gtid_set, 1);
来让 slave 去判断对 master 执行过的 gtid_set 是否已经同步完成。
wait_for_executed_gtid_set
函数的返回结果解析如下,
如果 slave 执行的事务中包含传入的 gtid_set,返回 0。
如果等待 1s 后还没同步完成,则返回 1。
所以在 GTID 模式下的,在判断是否应该在写入数据后读从库的逻辑,我们可以这样来写,
1, 在 master 写入数据后立马执行 show master status
,可以获取如下结果
可以看到 master 的 Executed_Gtid_Set 的值。
2, 在 slave 上执行
如果 1s 内没有返回,则直接返回 1。
3, 在上一步如果触发超时即返回 1,则直接读取主库,如果是=0 ,则直接读取从库。这样便能最大程度从从库读取数据。
文章转载自:蓝胖子的编程梦
评论