MySQL基础第十七篇

135 阅读4分钟

读写分离有哪些坑?

读写分离的主要目的就是为了分摊主库的压力。由于读库和写库分离出来,就变成了多个库,这就会有主从的问题,例如主从同从可能存在延迟,客户端在执行完一个更新事物的后马上发起查询,如果查询选择的是从库的话,就很有可能读到刚刚事物之前的状态。

上面这“在从库上会读到系统的一个过期状态的现象”,我们暂且称之为过期读。

事实上,我们没办法完全100%避免主从延迟,我们只能根据具体环境匹配一个最佳的方案。一下例举出来一些常用的方案:

1.强制走主库方案;

对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库。对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库。当然,这个方案最大的问题在于,有时候你会碰到“所有查询都不能是过期读”的需求,比如一些金融类的业务。这样的话,你就要放弃读写分离,所有读写压力都在主库,等同于放弃了扩展性。

2.sleep 方案;

主库更新后,读从库之前先 sleep 一下。具体的方案就是,类似于执行一条 select sleep(1) 命令。这个方案的假设是,大多数情况下主备延迟在 1 秒之内,做一个 sleep 可以有很大概率拿到最新的数据。

3.判断主备无延迟方案;

第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。

第二种方法,对比位点确保主备无延迟: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 集合。如果这两个集合相同,也表示备库接收到的日志都已经同步完成。

4.配合 semi-sync 方案;

要解决这个问题,就要引入半同步复制,也就是 semi-sync replication。semi-sync 做了这样的设计:事务提交的时候,主库把 binlog 发给从库;从库收到 binlog 以后,发回给主库一个 ack,表示收到了;主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。也就是说,如果启用了 semi-sync,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。

5.等主库位点方案;

select master_pos_wait(file, pos[, timeout]);

这条命令的逻辑如下:它是在从库执行的;参数 file 和 pos 指的是主库上的文件名和位置;timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。这个命令正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。当然,除了正常返回一个正整数 M 外,这条命令还会返回一些其他结果,包括:如果执行期间,备库同步线程发生异常,则返回 NULL;如果等待超过 N 秒,就返回 -1;如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0。

6.等 GTID 方案;

select wait_for_executed_gtid_set(gtid_set, 1);

这条命令的逻辑是:等待,直到这个库执行的事务中包含传入的 gtid_set,返回 0;超时返回 1。在前面等位点的方案中,我们执行完事务后,还要主动去主库执行 show master status。而 MySQL 5.7.6 版本开始,允许在执行完更新类事务后,把这个事务的 GTID 返回给客户端,这样等 GTID 的方案就可以减少一次查询。这时,等 GTID 的执行流程就变成了:trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;选定一个从库执行查询语句;在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);如果返回值是 0,则在这个从库执行查询语句;否则,到主库执行查询语句。跟等主库位点的方案一样,等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。