主从延迟如何解决

最近项目上线,遇到了主从问题。按理说公司基建不至于出现这种问题,但就是出现了。可能因为用的不是原生的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语言
相关推荐
你的人类朋友9 分钟前
浅谈Object.prototype.hasOwnProperty.call(a, b)
javascript·后端·node.js
仙灵灵32 分钟前
前端的同学看过来,今天讲讲jwt登录
前端·后端·程序员
Home32 分钟前
一、Java性能优化--Nginx篇(一)
后端
陈随易34 分钟前
VSCode v1.99发布,王者归来,Agent和MCP正式推出
前端·后端·程序员
ShooterJ35 分钟前
海量序列号的高效处理方案
后端
你的人类朋友38 分钟前
CommonJS模块化规范
javascript·后端·node.js
小码编匠1 小时前
C# 实现西门子S7系列 PLC 数据管理工具
后端·c#·.net
Postkarte不想说话1 小时前
Ubuntu24.04搭建TrinityCore魔兽世界
后端
Weison1 小时前
Apache Doris Trash与Recover机制
后端
codelang3 小时前
Cline + MCP 开发实战
前端·后端