读写分离架构

客户端直连

Proxy

对比

  1. 客户端直连
    • 少了一层Proxy转发,查询性能稍微好一点
    • 整体架构简单,排查问题方便
    • 需要了解后端部署细节,在出现主从切换、库迁移时,客户端有感知,需要调整数据库连接信息
      • 一般伴随着一个负责管理后端的组件,例如ZooKeeper
  2. Proxy – 发展趋势
    • 对客户端友好,客户端不需要关注后端细节,但后端维护成本较高
    • Proxy也需要高可用架构,带Proxy的整体架构相对复杂

过期读

由于主从延迟,主库上执行完一个更新事务后,立马在从库上执行查询,有可能读到刚刚的事务更新之前的状态

解决方案

强制走主库

  1. 将查询请求做分类
    • 必须要拿到最新结果的请求,强制将其发送到主库上
    • 可以读到旧数据的请求,将其发到从库上
  2. 如果完全不能接受过期读,例如金融类业务,相当于放弃读写分离,所有的读写压力都在主库上

SLEEP

  1. 主库更新后,读从库之前先SLEEP一下,类似于SELECT SLEEP(1)
    • 基于的假设:大多数主从延时在1秒内
  2. 卖家发布商品后,用Ajax直接把客户端输入的内容作为“新的商品”显示在页面上,而非真正的做数据库查询
    • 等卖家再次刷新页面,其实主从已经同步完成了,也达到了SLEEP的效果
  3. SLEEP方案解决了类似场景下的过期读问题,但存在不精确的问题
    • 如果主从延时只有0.5秒,也会等到1秒
    • 如果主从延迟超过了1秒,依然会出现过期读的问题

判断主从无延迟

  1. SLOW SLAVE STATUS.Seconds_Behind_Master
    • 每次在从库执行查询请求前,先判断Seconds_Behind_Master是否等于0
    • Seconds_Behind_Master=0才能执行查询请求
    • Seconds_Behind_Master的精度为,如果需要更高精度,可以考虑对比位点GTID
  2. 位点
    • Master_Log_FileRead_Master_Log_Pos,表示读到的主库的最新位点
    • Relay_Master_Log_FileExec_Master_Log_Pos,表示从库执行的最新位点
    • Master_Log_File=Relay_Master_Log_FileRead_Master_Log_Pos=Exec_Master_Log_Pos
      • 表示接收到的日志已经同步完成
  3. GTID
    • Auto_Position=1,表示主从关系使用了GTID协议
    • Retrieved_Gtid_Set,表示从库收到的所有日志的GTID集合
    • Executed_Gtid_Set,表示从库所有已经执行完成GTID集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SHOW SLAVE STATUS\G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Log_File: master-bin.000003
Read_Master_Log_Pos: 484
Relay_Log_File: relay-bin.000003
Relay_Log_Pos: 699
Relay_Master_Log_File: master-bin.000003
Exec_Master_Log_Pos: 484
Seconds_Behind_Master: 0
Master_UUID: b0bda503-3cf1-11e9-8c3a-0242ac110002
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Retrieved_Gtid_Set: b0bda503-3cf1-11e9-8c3a-0242ac110002:1-6
Executed_Gtid_Set: b0bda503-3cf1-11e9-8c3a-0242ac110002:1-6,ba0b2f12-3cf1-11e9-9c40-0242ac110003:1-5
Auto_Position: 1

不精确

  1. binlog在主从之间的状态
    • 主库执行完成,写入binlog,反馈给客户端
    • binlog被主库发送到从库,从库收到
    • 从库执行binlog(应用relaylog
  2. 上面判断的主从无延迟:_从库收到的日志都执行完成了_
    • 并没有考虑这部分日志:客户端已经收到提交确认,但从库还未收到
  1. 主库上执行完成了3个事务:trx1trx2trx3
  2. trx1trx2已经传到从库,并且已经执行完成了
  3. trx3在主库执行完成后,并且已经回复给客户端,但还未传到从库中
  4. 如果此时在从库上执行查询请求,按上面的逻辑,从库已经没有同步延迟了,但还是查不到trx3的变更,出现了过期读

SEMI-SYNC

SEMI-SYNC设计

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

小结

  1. 启用了SEMI-SYNC
    • 所有给客户端发送过确认的事务,都确保了某一个从库已经收到了这个日志
  2. SEMI-SYNC+位点的方案,只针对一主一从的场景是成立的
    • 一主多从的场景里,主库只要等到一个从库的ACK,就开始给客户端返回确认
    • 如果对刚刚响应了ACK的从库执行查询请求(+判断主从无延迟),能够确保读到最新的数据,否则可能是过期读
  3. 如果在业务高峰期,主库的位点或者GTID集合更新很快,从库可能一直跟不上主库,导致从库迟迟无法响应查询请求
    • 在出现持续延迟的情况下,可能会出现过度等待(判断主从无延迟)的情况

等主库位点

1
SELECT MASTER_POS_WAIT(file, pos[, timeout]);
  1. 从库上执行
  2. 参数filepos指的是主库上的文件名和位置
  3. timeout单位为秒
  4. 返回正整数,表示从命令开始执行,到应用完filepos,总共执行了多少事务
    • 如果在执行期间,从库的同步线程发生异常,返回NULL
    • 如果等待超过timeout秒,返回-1
    • 如果刚开始执行的时候,发现已经执行过这个位置,返回0

样例

先在MySQL A执行trx1,然后在MySQL B执行查询请求

  1. 事务trx1更新完成后,马上执行SHOW MASTER STATUS,得到当前主库执行到的FilePosition
  2. 选择一个从库执行查询语句
  3. 在该从库上先执行SELECT MASTER_POS_WAIT(File, Position, 1)
  4. 如果返回值>=0,则直接在这个从库上执行查询语句
  5. 否则,在主库上执行查询语句
    • 一种退化机制,针对主从延时不可控的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- MySQL A
mysql> SHOW MASTER STATUS;
+-------------------+----------+--------------+------------------+------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+-------------------+----------+--------------+------------------+------------------------------------------+
| master-bin.000003 | 643 | | | b0bda503-3cf1-11e9-8c3a-0242ac110002:1-7 |
+-------------------+----------+--------------+------------------+------------------------------------------+

-- MySQL B
mysql> SELECT MASTER_POS_WAIT('master-bin.000003',643,1);
+--------------------------------------------+
| MASTER_POS_WAIT('master-bin.000003',643,1) |
+--------------------------------------------+
| 0 |
+--------------------------------------------+

等GTID

1
2
3
-- 等待,直到这个库执行的事务中包含传入的gtid_set,返回0
-- 超时返回1
SELECT WAIT_FOR_EXECUTED_GTID_SET(gtid_set, 1);

样例

  1. 从MySQL 5.7.6开始,允许在执行完更新类事务后,把这个事务的GTID返回给客户端
  2. 事务trx1更新完成后,从返回结果中直接获取trx1GTID,记为gtid1
  3. 选择一个从库执行查询语句
  4. 在该从库上先执行SELECT WAIT_FOR_EXECUTED_GTID_SET(gtid1, 1)
  5. 如果返回0,则直接在这个从库上执行查询语句
  6. 否则,在主库上执行查询语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- MySQL A
mysql> SHOW MASTER STATUS;
+-------------------+----------+--------------+------------------+------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+-------------------+----------+--------------+------------------+------------------------------------------+
| master-bin.000003 | 643 | | | b0bda503-3cf1-11e9-8c3a-0242ac110002:1-7 |
+-------------------+----------+--------------+------------------+------------------------------------------+

-- MySQL B
mysql> SELECT WAIT_FOR_EXECUTED_GTID_SET('b0bda503-3cf1-11e9-8c3a-0242ac110002:1-7',1);
+--------------------------------------------------------------------------+
| WAIT_FOR_EXECUTED_GTID_SET('b0bda503-3cf1-11e9-8c3a-0242ac110002:1-7',1) |
+--------------------------------------------------------------------------+
| 0 |
+--------------------------------------------------------------------------+

参考资料

《MySQL实战45讲》