概述
读写分离的主要目标是分摊主库的压力,上图的结构是客户端主动做负载均衡,由客户端来选择后端数据进行查询;
另一种架构是MySQL和客户端之间有一个中间代理层proxy,客户端只连接proxy,由proxy根据请求类型和上下文决定请求的分发路由。
两种方案:
客户端直连方案:
查询性能好一点,整体架构简单便于排查问题。但尽心主备切换、库迁移等需要调整客户端连接信息,需要伴随一个负载管理后端的组件,例如Zookeeper,尽量让业务端只专注于业务逻辑开发;
proxy架构
对客户端比较友好。客户端不需要关注后端细节,连接维护、后端信
息维护等工作,都是由 proxy 完成的。但这样的话,对后端维护团队的要求会更高。而
且,proxy 也需要有高可用架构。
但是两种方案都可能会有一个问题,就是"过期读"
由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态
下面是解决过期读的一些方案。
- 强制走主库方案
- sleep方案
- 判断主备无延迟方案
- 配合semi-sync方案
- 等GTID方案
强制走主库方案
强制走主库方案其实就是,将查询请求做分类:
- 对于必须要拿到最新结果的请求,强制将其发到主库上;例如,交易平台上卖家发布商品之后,马上返回主界面查看商品是否发布成功,这个请求需要拿到最新的结果,必须走主库;
- 对于可以读到旧数据的请求,才将其发到从库上;比如,买家逛商铺页面,晚几秒看到新发布的商品也是可以接受的;
Sleep方案
主库更新后,读从库前先sleep一下,大多数情况下主备延迟在一秒内,做个sleep可以有很大概率拿到最新的数据;
以卖家发布商品为例,商品发布后,用 Ajax(Asynchronous JavaScript + XML,异步
JavaScript 和 XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真
正地去数据库做查询。
卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去
查看商品的时候,其实已经过了一段时间,也就达到了 sleep 的目的,进而也就解决了过期读的问题。
判断主备无延迟方案
要确保备库无延迟,通常有三种做法:
-
确保主备无延迟,每次从库执行查询请求前,先判断
seconds_behind_master 是否已经等于 0,等到该值为0才能执行查询请求; -
对比位点确保主备无延迟;
-
对比GTID确保主备无延迟
GTID:全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。Auto_Position=1 ,表示这对主备关系使用了 GTID 协议。
Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;
Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合
配合semi-sync
引入半同步复制,也就是 semi-sync replication
设计如下:
- 事务提交的时候,主库把 binlog 发给从库;
- 从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
- 主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。
如果启用了semi-sync,表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。
等GTID方案
如果你的数据库开启了GTID方案,对应的也有等待GTID的方案
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,则在这个从库执行查询语句;
- 否则,到主库执行查询语句
总结
本文介绍了读写分离,一主多从,可能会遇到过期读的问题,以及相关的解决方案;