主从延迟如何解决

最近项目上线,遇到了主从问题。按理说公司基建不至于出现这种问题,但就是出现了。可能因为用的不是原生的MySQL吧。主从延迟会给前端和服务端带来很多问题,需要花费时间用工程手段来解决,我认为这是很不合理的。

举几个因为主从延迟会导致问题场景:

  1. 创建了一个商品然后立即跳转到详情页
  2. 在列表页更新了用户的权限,立即刷新

凡是像这种操作后立即获取的,全会有问题。

为什么要有主从

MySQL数据库的主从(Master-Slave)架构主要是为了实现数据的高可用性(High Availability)和读写分离,具体的原因如下:

  1. 数据备份:主从架构可以实现数据的实时备份,从库可以作为主库的一个镜像存在,当主库出现问题时,可以迅速切换到从库,保证数据的安全性。
  2. 读写分离:在主从架构中,主库主要负责写操作,从库主要负责读操作,这样可以分担主库的压力,提高系统的处理能力。
  3. 故障切换:当主库出现故障时,可以迅速切换到从库,保证服务的连续性,提高系统的可用性。
  4. 负载均衡:通过主从架构,可以将读请求分散到多个从库,实现负载均衡,提高系统的性能。
  5. 数据一致性:主从复制可以确保数据在主库和从库之间保持一致,提高数据的准确性。 因此,为了保证数据的安全性和系统的高可用性,MySQL通常会采用主从架构。

主从如何同步

下面是一个update 语句在主节点 A 执行,然后同步到从节点 B 的完整流程图

从库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务从库 B的这个长连接。一个事务日志同步的完整过程是这样的:

  1. 在从库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。

  2. 在从库 B 上执行 start slave 命令,这时候从库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。

  3. 主库 A 校验完用户名、密码后,开始按照从库 B 传过来的位置,从本地读取 binlog,发给 B。

  4. 从库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。

  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

为什么会主从延迟

什么是主从延迟?

  1. 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;

  2. 之后传给从库 B,我们把从库 B 接收完这个 binlog 的时刻记为 T2;

  3. 从库 B 执行完成这个事务,我们把这个时刻记为 T3。

所谓主从延迟,就是同一个事务,在从库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。

你可以在从库上执行 show slave status 命令,它的返回结果里面会显示seconds_behind_master,用于表示当前从库延迟了多少秒。

在网络正常的时候,日志从主库传给从库所需的时间是很短的,即 T2-T1的值是非常小的。也就是说,网络正常情况下,主从延迟的主要来源是从库接收完 binlog和执行完这个事务之间的时间差。

主从延迟来源

  1. 有些部署条件下,从库所在机器的性能要比主库所在的机器性能差。
  2. 从库的压力大。如从库上的查询耗费了大量的 CPU 资源,影响了同步速度,造成主从延迟。
  3. 大事务。因为主库上必须等事务执行完成才会写入 binlog,再传给从库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10分钟。
  4. 大表 DDL,也是典型的大事务场景。
  5. 从库的并行复制能力。查看软件版本,在官方的 5.6 版本之前,MySQL (sql_thread)只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主从延迟问题。

如何解决

一般的主从结构如下:

一旦出现主从延迟问题,有如下解决方案

强制走主库方案 - 有点可行

  1. 将查询请求做分类,必须拿到最新结果的,强制请求到主库
  • 优点:能够区分场景,压力可控
  • 缺点:
    • 前后端都得改动。前端判断是否走主库,服务端判断指定场景走查主库
    • 后续维护也比较麻烦
    • 有时前端无法判断出场景,如进详情页,前端无法判断是刚创建完跳转的还是打开的是早已创建的
  1. 全部走主库
  • 优点:简单便捷
  • 缺点:不区分具体场景,主库压力大
  1. 先读从库,从库没有读主库
  • 优点:相对简单
  • 缺点:无法处理所有场景,如list的场景,因为必然有数据,但并不知道是否准确

sleep 方案 - 有点可行

  1. 前端延迟请求
  • 优点:简单便捷
  • 缺点:用户体验不好
  1. 写相关接口,服务端返回结果详情,前端展示详情。如创建商品接口,服务端返回商品的详细信息,前端直接展示详细信息,不请求接口
  • 优点:逻辑比较通顺、清晰
  • 缺点:
    • 前端需要实现多套逻辑。如虽然是详情,但至少可能来自创建和详情接口;在成员列表中删除member成功后就直接不显示,不再调用list接口
    • 维护成本高,如对于详情页,如果后续详情也变更,创建和详情接口如何保持一致
    • 无法处理所有场景,如创建空间后获取成员列表(至少有自己)

判断主从延迟方案 - 有的可行

  1. 写操作写redis(过期时间1~2s),读的时候判断是否有redis,有则读主库。如创建商品,则在redis里记录商品id,获取详情的时候先判断该商品id是否在redis存在,如果存在则读主库。
  • 优点:
    • 前端无需改动,服务端改动相对可控,而且设置为弱依赖,所以问题应该不大
    • 能解决大部分问题
  • 缺点:
    • 服务端需要根据场景记录、读取redis
    • 有一定概率增加主库压力,但总体可控
  1. 判断主从无延迟方案 - 不可行
  • 每次从库执行查询请求前,先判断seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为0 才能执行查询请求。可以使用 show slave status。
  • 对比位点确保主从无延迟
  • 对比 GTID 集合确保主从无延迟

这种方式感觉不太现实

  • 实现上成本高
  • 仍然有过期读问题。因为上面判断主从无延迟的逻辑,是"从库收到的日志都执行完成了"。但是,从 binlog在主从之间状态的分析中,不难看出还有一部分日志,处于客户端已经收到提交确认,而从库还没收到日志的状态。
  • 如果在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况
  1. 配合 semi-sync(半同步复制) 方案 - 不可行

主从无延迟方案 + semi-sync 方案 能解决过期读问题。

semi-sync 做了这样的设计:

  • 事务提交的时候,主库把 binlog 发给从库;

  • 从库收到 binlog 以后,发回给主库一个 ack,表示收到了;

  • 主库收到这个 ack 以后,才能给客户端返回"事务完成"的确认。

这样,semi-sync 配合前面关于位点的判断,就能够确定在从库上执行的查询请求,可以避免过期读。

但这种方案也不太现实,而且没有完全解决问题

  • semi-sync+ 位点判断的方案,只对一主一从的场景是成立的。如果落到其它从库,还是会出现过期读
  1. 等主库位点方案 - 不可行
csharp 复制代码
select master_pos_wait(file, pos[, timeout]);

这条命令的逻辑如下:

  • 它是在从库执行的;

  • 参数 file 和 pos 指的是主库上的文件名和位置;

  • timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。

返回结果如下:

  • 如果执行期间,从库同步线程发生异常,则返回 NULL;

  • 如果等待超过 N 秒,就返回 -1;

  • 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0。

  • 正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。

使用方法如下

  • trx1 事务在主库更新完成后,马上执行 show master status 得到当前主库执行到的 File 和Position;

  • 选定一个从库执行查询语句;

  • 在从库上执行 select master_pos_wait(File, Position, 1);

  • 如果返回值是 >=0 的正整数,则在这个从库执行查询语句

这种也不太现实,想想实现复杂度有多高。

  1. 等 GTID 方案 - 不可行
csharp 复制代码
 select wait_for_executed_gtid_set(gtid_set, 1);

这条命令的逻辑是:

  • 等待,直到这个库执行的事务中包含传入的 gtid_set,返回 0;

  • 超时返回 1。

等 GTID 的执行流程为:

  • trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;

  • 选定一个从库执行查询语句;

  • 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);

  • 如果返回值是 0,则在这个从库执行查询语句;

  • 否则,到主库执行查询语句。

总结

真的是基建要做好,基建好了大家能把精力放到更重要的事情上。如果真出现主从延迟问题,能选择的方案其实比较少。要么就分场景走主库、要么前端sleep(偏临时方案)、要目服务端自己判断一下主从延迟情况。

这次我们选择redis打点记录,看看效果怎么样吧。

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言
相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
mqiqe3 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
工业甲酰苯胺4 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
BestandW1shEs4 小时前
谈谈Mysql的常见基础问题
数据库·mysql
重生之Java开发工程师4 小时前
MySQL中的CAST类型转换函数
数据库·sql·mysql
教练、我想打篮球4 小时前
66 mysql 的 表自增长锁
数据库·mysql
Ljw...4 小时前
表的操作(MySQL)
数据库·mysql·表的操作
2401_857610034 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_4 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis