数据库_读写分离

目前数据库提高并发量最简单的方式就是使用读写分离。但读写分离时会引入读旧数据的问题(数据不一致)的问题。整理了下大概有以下几种解决方案。按照解决成本(成本从低到高)依次排序。

忍受法

方案描述:不管他,没有读到最新数据也没事。不论业务还是开发都不需要特殊改造。
优点: 简单。
缺点:存在读取到旧数据可能,牺牲部分实时性,

产品业务调整法(伪Sleep法)

方案描述:
主库更新后,读从库之前先sleep一下。类似执行一条select sleep(1)命令。(假设大多情况下主备延迟在1秒之内)。
大多数是通过对业务逻辑的调整。实现类似“sleep”的效果。
比如:
以卖家发布商品为例,商品发布后,用Ajax(Asynchronous JavaScript + XML,异步JavaScript和XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。
也就是说,这个sleep方案确实解决了类似场景下的过期读问题。
优点:较低成本解决问题。
缺点:
01,业务需要做调整,部分操作可能需要用户多一次交互操作。需要有一定研发背景。
02,如果用户交互很快,依然有可能导致读取旧数据,不能严格的确保问题被解决

选择性强制读主

方案描述:
将查询请求分为这么两类:
第一种:必须要拿到最新结果的请求,强制将其发到主库上。
第二种:可以读到旧数据的请求,才将其发到从库上。
优点: 简单可靠,严谨。
缺点:
01,开发需要时刻清楚那些用主,那些用从。
02,有时候你会碰到“所有查询都不能是过期读”的需求,比如一些金融类的业务。这样的话,你就要放弃读写分离,所有读写压力都在主库,等同于放弃了扩展性。

点位对比法(无延迟读从,有延迟读主)

先判断主备是否有延迟,无延迟后才会从从库读取,如果有延迟则读取主库。

判断主备无延迟方案

第一种:seconds_behind_master是否等于0
如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。seconds_behind_master的单位是秒。

第二种方法,对比位点确保主备无延迟:
Master_Log_File和Read_Master_Log_Pos,表示的是读到的主库的最新位点;
Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是备库执行的最新位点。
如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同,就表示接收到的日志已经同步完成。

第三种方法,对比GTID集合确保主备无延迟:
Auto_Position=1 ,表示这对主备关系使用了GTID协议。
Retrieved_Gtid_Set,是备库收到的所有日志的GTID集合;
Executed_Gtid_Set,是备库所有已经执行完成的GTID集合。
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。
对比位点法就是完美的?是否存在其他隐患?
第一个问题:虚假同步(GTID集合为例)
输入图片说明
如果这时候你在从库B上执行查询请求,按照我们上面的逻辑,从库认为已经没有同步延迟,但还是查不到trx3的。严格地说,就是出现了过期读。
第二个问题:持续不同步
如果在业务更新的高峰期,主库的位点或者GTID集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况。
那么,这个问题有没有办法解决呢? 需要引入semi-sync (半同步复制)

semi-sync

semi-sync做了这样的设计:

1
2
3
事务提交的时候,主库把binlog发给从库;  
从库收到binlog以后,发回给主库一个ack,表示收到了;
主库收到这个ack以后,才能给客户端返回“事务完成”的确认。

实际上,回到我们最初的业务逻辑里,当发起一个查询请求以后,我们要得到准确的结果,其实并不需要等到“主备完全同步”。
到这里,我们小结一下,semi-sync配合判断主备无延迟的方案,存在两个问题:

1
2
一主多从的时候,在某些从库执行查询请求会存在过期读的现象;  
在持续延迟的情况下,可能出现过度等待的问题。

接下来,我要和你介绍的等主库位点方案,就可以解决这两个问题。

等主库位点方案

基于位点的方案

要理解等主库位点方案,我需要先和你介绍一条命令:
select master_pos_wait(file, pos[, timeout]);
这条命令的逻辑如下:

1
2
3
它是在从库执行的;  
参数file和pos指的是主库上的文件名和位置;
timeout可选,设置为正整数N表示这个函数最多等待N秒。

这个命令正常返回的结果是一个正整数M,表示从命令开始执行,到应用完file和pos表示的binlog位置,执行了多少事务。
当然,除了正常返回一个正整数M外,这条命令还会返回一些其他结果,包括:

1
2
3
如果执行期间,备库同步线程发生异常,则返回NULL;  
如果等待超过N秒,就返回-1;
如果刚开始执行的时候,就发现已经执行过这个位置了,则返回0。

对于图5中先执行trx1,再执行一个查询请求的逻辑,要保证能够查到正确的数据,我们可以使用这个逻辑:

1
2
3
4
5
trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;
选定一个从库执行查询语句;
在从库上执行select master_pos_wait(File, Position, 1);
如果返回值是>=0的正整数,则在这个从库执行查询语句;
否则,到主库执行查询语句。

你可能会说,如果所有的从库都延迟超过1秒了,那查询压力不就都跑到主库上了吗?确实是这样。
但是,按照我们设定不允许过期读的要求,就只有两种选择,一种是超时放弃,一种是转到主库查询。具体怎么选择,就需要业务开发同学做好限流策略了。

基于GTID的方案

如果你的数据库开启了GTID模式,对应的也有等待GTID的方案。
MySQL中同样提供了一个类似的命令:

1
select wait_for_executed_gtid_set(gtid_set, 1);

这条命令的逻辑是:

1
2
等待,直到这个库执行的事务中包含传入的gtid_set,返回0;
超时返回1。

参考

28 | 读写分离有哪些坑?:https://www.cnblogs.com/a-phper/p/10313967.html
数据库读写分离这个坑,让刚入职的我一脸懵逼!:https://hollis.blog.csdn.net/article/details/113100859

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×