读写分析的主要目标就是分摊主库的压力,一主多从是读写分离的基本架构。
上图的结构是客户端主动来做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层,也就是让客户端选择后端数据库进行查询。
还有一种架构,在mysql和客户端之间有一个中间代理层proxy,客户端只连接proxy,由proxy根据请求类型和上下文决定请求的分发路由。
这两个方案的优缺点如下:
- 客户端直连。少了一层proxy转发,所以查询性能稍微好一点,并且整体架构简单。不过这种方案,在主备切换、库迁移的时候,客户端都会感知到,并且需要调整数据库连接信息。
- 带proxy的结构,对客户端比较友好,复杂的内容都在proxy完成并且proxy也需要由高可用架构。
不过目前的发展趋势是想着proxy架构的方向发展的。但是不论哪种架构都会存在问题:由于主从可能存在延迟,客户端执行完一个更新事务之后马上发起查询,如果选择的是从库的话,就有可能督导刚刚的事务更新之前的状态。
这种“在从库上会读到系统的一个过期状态”的现象,这里称之为“过期读”。
下面就介绍处理过期读的方案。
强制走主库方案
将请求进行分类。
- 对于必须拿到最新结果的请求,强制将其发送到主库上。
- 对于可以读到就数据的请求,才将其发送到从库上。
这个方案也是用的最多的。不过对于金融类的数据,所有的查询都不能是过期读,那么这个方案就不合适了。
sleep方案
主库更新后,读从库之前先sleep一下,类似执行一条select sleep(1)的命令。
这个方案是假设大多数情况下主备延迟在1秒之内。不过看着有些不靠谱。
判断主备无延迟方案
在如何保证高可用中使用了show slave status结果里的seconds_behind_master参数值,可以用来衡量主备延迟时间的长短。
每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0,如果还不等于0,那就必须等到这个参数变为0才能够执行查询请求。seconds_behind_master的单位是秒,如果精度不够可以采用下面的方案。
对比主库和备库的位点,如果相同则表示备库接收到的日志已经同步完成。
对比GTID集合确保主备无延迟,备库收到的所有日志的GTID集合与备库已经执行完成的GTID集合相同,就可以表示备库接受到的日志已经同步完成(网络正常情况下,同步的延迟主要是备库从接受到完成的binlog到执行完成的时间差)。 但是可能存在主库已经提交,但是还没有发送给从库的日志,所以这种策略还是存在延迟的。
semi-sync
引入半同步复制,也就是semi-sync replication。semi-sync的设计如下:
- 事务提交的时候,主库把binlog发送给从库。
- 从库收到binlog以后,发挥给主库一个ack表示收到了
- 主库收到这个ack以后,才能给客户端返回“事务完成”的确认。
有些类似tcp握手的流程,通过这种方法,确保了主库上确认的事务,从库上都收到了日志。
这种方法与位点相结合可以解决过期读的问题了,但是只适合一主一从的结构,因为收到一个从库的ack之后,主库就可以返回确认给客户端了。
如果是一主多从,那么某些从库上请求还会存在过期读。
等待主库位点方案
select master_pos_wait(file, pos[, timeout]);这条命令逻辑如下:
- 在从库上执行
- 参数file和pos指的是主库上的文件名和位置。
- timeout可选,设置为正整数N表示这个函数最多等待N秒
这个命令返回的正常结果是一个正整数M,表示从命令开始执行,到应用完file和pos表示的binlog位置,执行了多少事务。
现在对于更新后立即查询请求的逻辑,按照下面的逻辑执行:
- 事务更新完成后,马上执行show master status得到当前主库执行到的file和Position
- 选定一个从库执行查询语句
- 在从库上执行select master_post_wait(File,Position, 1);
- 如果返回值是>=0的正整数,则在这个从库执行查询语句;
- 否则,到主库执行查询语句。
GTID方案
数据库开启了GTID模式,也有对应的GTID方案。 mysql中也有一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);这个命令的逻辑是:
- 等待,直到这个库执行的事务中包含传入的gtid_set,返回0
- 超时返回1.
这里不需要像等待位点一样执行完事务还要主动去主库执行show mater status。mysql5.7.6版本允许在执行完更新类事务之后,把这个事务的GTID返回给客户端,这样等GTID的方案就可以减少一次查询。
等GTID的执行流程就变更为:
- 事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1.
- 选定一个从库执行查询语句。
- 在从库上执行select wait_for_executed_gtid_set(gtid1, 1);
- 如果返回值是0,则在这个从库执行查询语句
- 否则,到主库执行查询语句。