读写分离的主要目标就是分摊主库的压力。
1)客户端(client)主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。也就是说,由客户端来选择后端数据库进行查询。这种方案,由于要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息。一般采用这样的架构,一定会伴随一个负责管理后端的组件,比如 Zookeeper,尽量让业务端只专注于业务逻辑开发。
2)在 MySQL 和客户端之间有一个中间代理层 proxy,客户端只连接 proxy, 由 proxy 根据请求类型和上下文决定请求的分发路由。客户端不需要关注后端细节,连接维护、后端信息维护等工作,都是由 proxy 完成的。对后端维护团队的要求会更高,proxy 也需要有高可用架构,整体就相对比较复杂。
趋势是往带 proxy 的架构方向发展的
过期读:主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询从库的话,可能读到刚刚的事务更新之前的状态。
强制走主库方案
强制走主库方案其实就是,将查询请求做分类。对于必须要拿到最新结果的请求,强制将其发到主库上。所有查询都不能是过期读”的需求,就要放弃读写分离。
Sleep 方案
主库更新后,读从库之前先 sleep 一下。具体的方案就是,类似于执行一条 select sleep(1) 命令。
以卖家发布商品为例,商品发布后,用 Ajax(Asynchronous JavaScript + XML,异步 JavaScript 和 XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。这样,卖家就可以通过这个显示,来确认产品已经发布成功了。
等到卖家再刷新页面,去查看商品的时候,其实已经过了一段时间,也就达到了 sleep 的目的,进而也就解决了过期读的问题。
判断主备无延迟方案
show slave status 结果里的 seconds_behind_master 参数的值,可以用来衡量主备延迟时间的长短。确保主备无延迟的方法有:
第一种,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。
第二种,对比位点:Master_Log_File 和 Read_Master_Log_Pos,主库的最新位点;Relay_Master_Log_File 和 Exec_Master_Log_Pos,备库执行的最新位点。这两组值完全相同,就表示接收到的日志已经同步完成。
第三种方法,对比 GTID 集合确保主备无延迟:Auto_Position=1 ,表示这对主备关系使用了 GTID 协议。Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合。这两个集合相同,也表示备库接收到的日志都已经同步完成。
第二三种方式比第一种方式更加精准。
上面判断主备无延迟的逻辑,是“备库收到的日志都执行完成了”。但是,从 binlog 在主备之间状态的分析中,不难看出还有一部分日志,处于客户端已经收到提交确认,而备库还没收到日志的状态。
semi-sync
semi-sync 做了这样的设计:
1)事务提交的时候,主库把 binlog 发给从库;
2)从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
3)主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。
semi-sync+ 位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认。一主多从的时候,在某些从库执行查询请求会存在过期读的现象;
等主库位点方案
select master_pos_wait(file, pos[, timeout]);
1)它是在从库执行的;
2)参数 file 和 pos 指的是主库上的文件名和位置;
3)timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。
4)正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。
具体应用的方法:
1)trx1 事务更新完成后,马上执行 show master status 得到当前主库执行到的 File 和 Position;
2)选定一个从库执行查询语句;在从库上执行 select master_pos_wait(File, Position, 1);
3)如果返回值是 >=0 的正整数,则在这个从库执行查询语句;否则,到主库执行查询语句。
比如这条 select 查询最多在从库上等待 1 秒。那么,如果 1 秒内 master_pos_wait 返回一个大于等于 0 的整数,就确保了从库上执行的这个查询结果一定包含了 trx1 的数据。
GTID 方案
数据库开启了 GTID 模式,MySQL 中提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
MySQL 5.7.6 版本开始,允许在执行完更新类事务后,把这个事务的 GTID 返回给客户端
等 GTID 的执行流程就变成了:
1)trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;
2)选定一个从库执行查询语句;在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
3)如果返回值是 0,则在这个从库执行查询语句;否则,到主库执行查询语句。
怎么能够让 MySQL 在执行事务后,返回包中带上 GTID 呢?需要将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 。