读写分离有哪些坑?
读写分离存在的问题,主要是从库不可避免存在同步延迟,导致客户端在从库读取到旧数据。
读写分离架构
读写分离主要目的时分摊主库的压力。
上面的结构是client主动选择后端数据库。
还有一种结构是带Proxy的读写分离架构
客户端直连和带proxy读写分离架构的优缺点
-
客户端直连结构简单,相比proxy少了一层转发,性能好一点。缺点是clent和后端架构耦合严重,比如主备切换,库迁移都需要调整客户端。
-
带proxy的架构,相当于是hold住了后端的所有细节。对技术要求高,比如要求proxy高可用
过期读的问题
由于主从延迟的问题,从库的读取到的结果可能是旧的。
有几种方案可以解决:
- 强制走主库方案;
- sleep 方案;
- 判断主备无延迟方案;
- 配合 semi-sync 方案;
- 等主库位点方案;等 GTID 方案。
强制走主库
对请求做分类,将需要马上拿到最新结果的查询放到主库查询,其他请求放到从库执行。
Sleep方案
从库查询前执行前执行一次Sleep(1)。感觉不太靠谱
判断主备无延迟方案
-
show slave status 查看seconds_behind_master ,判断seconds_bebind_master是否为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 集合。
如果Retrieved_Gtid_Set = Executed_Gtid_Set 表示备库接受到的日志都已经同步完成。
GTID 集合表示的是从库上已经收到的事务日志,对于主库已经执行完成,给客户端确认,但是还没通过binlog发给从库执行,这部分内容如果客户端到从库读取会发现还没同步到从库的
配合semi-sync
半同步确认指的是主库提交binlog后,要等至少一个从库确认收到了确认才会给客户端确认。
开启semi-sync 配置前面的对比位点或对比gtid集合,就能保证从库不会出现过期读。但是只适合一主一从的架构,如果有多个从库,由于半同步是只要一个从库有确认,就给客户端确认,那么其他没有得到确认的从库还是会出现过期读。
到这里,我们小结一下,semi-sync 配合判断主备无延迟的方案,存在两个问题:
- 一主多从的时候,在某些从库执行查询请求会存在过期读的现象;
- 在持续延迟的情况下,可能出现过度等待的问题。
等主库位点方案
select master_pos_wait(file, pos[, timeout]);
上面这条命令是在从库执行,表示在timeout时间内,等主库binlog文件file执行到pos位置返回执行了多少事务,如果是>=0 表示pos已经被执行了,超时则返回-1.
利用上面这个命令,我们有了等主库位点方案
- 执行完事务后,马上执行show master status, 得到主库执行到的File和Position
- 在从库执行select master_pos_wait(file, position, 1);
- 如果上面的语句返回时大于等于0,就在这个从库执行查询语句
- 否则到主库查询。
这个方案有个缺点就是,如果从库等主库位点都超时了,主库要做限流策略
GTID方案
MySQL 5.7.6 版本开始,允许在执行完更新类事务后,把这个事务的 GTID 返回给客户端,这样等 GTID 的方案就可以减少一次查询。
客户端在从库执行
select wait_for_executed_gtid_set(gtid_set, 1);
等待,直到这个库执行的事务中包含传入的 gtid_set,返回 0;超时返回 1。
问题是,怎么能够让 MySQL 在执行事务后,返回包中带上 GTID 呢?你只需要将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可。
高级用法: https://dev.mysql.com/doc/refman/5.7/en/c-api-functions.html