读写分离

主从复制

异步复制

  • 在主库开启 binlog 的情况下,如果主库有增删改的语句,会记录到 binlog 中。
  • 主库通过 IO 线程把 binlog 里面的内容传给从库的中继日志(relay log)中,主库给客户端返回 commit 成功(不管从库是否已经收到了事务的 binlog)
  • 从库的 SQL 线程负责读取 relay log 并应用到从库数据库中。

半同步复制

  • 在主库开启 binlog 的情况下,如果主库有增删改的语句,会记录到 binlog 中。
  • 主库通过 IO 线程把 binlog 里面的内容传给从库的中继日志(relay log)中,从库收到 binlog 后,发送给主库一个 ACK,表示收到了,主库收到这个 ACK 以后,才能给客户端返回 commit 成功
  • 从库的 SQL 线程负责读取 relay log 并应用到从库数据库中。

主从切换

可靠性优先策略

  1. 判断备库的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步;
  2. 把主库改成只读状态,把readonly设为true;
  3. 判断备库的seconds_behind_master的值,直到这个值变成0为止;
  4. 把备库改成可读写状态,把readonly 设为false;
  5. 把业务请求切到备库B。

如果一开始主备延迟就长达30分钟,而不先做判断直接切换的话,系统的不可用时间就会长达30分钟,这种情况一般业务都是不可接受的。

可用性优先策略

不等主备数据同步,直接把连接切到备库B,并且让备库B可以读写,那么系统几乎就没有不可用时间了。

可能出现不一致的数据。

设置binlog_format=row,会出现duplicate key error并停止;

使用mixed或者statement格式的binlog时,数据很可能悄悄地就不一致了

备库并行复制策略

按库并行

用于决定分发策略的hash表里,key就是数据库名。

如果你的主库上的表都放在同一个DB里面,这个策略就没有效果了;

或者如果不同DB的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。

MariaDB的并行复制策略

利用redo log组提交特性:

  • 能够在同一组里提交的事务,一定不会修改同一行;

  • 主库上可以并行执行的事务,备库上也一定是可以并行执行的。

实现方法

  1. 在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
  2. commit_id直接写到binlog里面;
  3. 传到备库应用的时候,相同commit_id的事务分发到多个worker执行;
  4. 这一组全部执行完成后,coordinator再去取下一批。

MySQL 5.7.22的新增并行复制策略

基于WRITESET的并行复制。

参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。

  1. COMMIT_ORDER,根据同时进入prepare和commit来判断是否可以并行的策略。
  2. WRITESET,表示的是对于事务涉及更新的每一行,计算出它的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行。
  3. WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。

主从延迟

可能原因

  • 大表 DDL
  • 大事务
  • 主库 DML 并发大
  • 从库配置差

判断主从延迟

1、判断 Seconds_Behind_Master 是否等于 0。

2、对比位点,Master_Log_File 跟 Relay_Master_Log_File 相等,并且 Read_Master_Log_Pos 跟 Exec_Master_Log_Pos 相等。

3、对比GTID,对比 Retrieved_Gtid_Set 和 Executed_Gtid_Set 是否相等。

主备切换

基于位点的主备切换

考虑到切换过程中不能丢数据,所以找位点时,找一个“稍微往前”的。再通过判断,跳过那些在从库上已经执行过的事务。

通常情况下,我们在切换任务的时候,要先主动跳过这些错误,有两种常用的方法。

  • 主动跳过一个事务。
  • 通过设置slave_skip_errors参数,直接设置跳过指定的错误。
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
MASTER_LOG_FILE=$master_log_name
MASTER_LOG_POS=$master_log_pos

基于GTID的主备切换

GTID的全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。在GTID模式下,每个事务都会跟一个GTID一一对应。

CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1

主备切换逻辑,实例A’的GTID集合记为set_a,实例B的GTID集合记为set_b:

  1. 实例B指定主库A’,基于主备协议建立连接。
  2. 实例B把set_b发给主库A’。
  3. 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
    a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;
    b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B;
  4. 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行。

过期读

问题:由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。

强制走主库方案

  1. 对于必须要拿到最新结果的请求,强制将其发到主库上
  2. 对于可以读到旧数据的请求,才将其发到从库上。

sleep方案

大多数情况下主备延迟在1秒之内,主库更新后,读从库之前先sleep一下。具体的方案就是,类似于执行一条select sleep(1)命令。

以卖家发布商品为例,商品发布后,用Ajax(Asynchronous JavaScript + XML,异步JavaScript和XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。这样,卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去查看商品的时候,其实已经过了一段时间,也就达到了sleep的目的,进而也就解决了过期读的问题。

缺点

  1. 如果这个查询请求本来0.5秒就可以在从库上拿到正确结果,也会等1秒;
  2. 如果延迟超过1秒,还是会出现过期读。

判断主备无延迟方案

  • 第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。

  • 第二种方法,对比位点确保主备无延迟(show slave status):
    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集合确保主备无延迟(show slave status):

    Auto_Position=1 ,表示这对主备关系使用了GTID协议。
    Retrieved_Gtid_Set,是备库收到的所有日志的GTID集合;
    Executed_Gtid_Set,是备库所有已经执行完成的GTID集合。

    缺点:仍可能过期读。备库收到的日志都执行完成了。还有一部分日志,处于客户端已经收到提交确认,而备库还没收到日志的状态。

配合semi-sync方案

引入半同步复制,配合前面关于位点的判断。

在一主多从场景中,如果是查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题。

在持续延迟的情况下,可能出现过度等待的问题。

等主库位点方案

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

等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。

等GTID方案

  1. trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;
  2. 选定一个从库执行查询语句;
  3. 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1)
  4. 如果返回值是0,则在这个从库执行查询语句;
  5. 否则,到主库执行查询语句。

等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。


   转载规则


《读写分离》 wangyixin-tom 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
分库分表 分库分表
垂直拆分和水平拆分的概述;分库分表的场景;
2021-03-15
下一篇 
MVCC MVCC
MVCC多版本并发控制。 MVCC 只在 RC 和 RR 两个隔离级别下工作。 不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。 Undo logundo
2021-03-14
  目录