一、简介
从mysql 8.0.14开始,组复制引进一个新的参数,group_replication_consistency
,事务一致性配置参数。用以控制在组复制中的事务一致性等级,其有5种可选配置项:
EVENTUAL(默认)
BEFORE_ON_PRIMARY_FAILOVER
BEFORE
AFTER
BEFORE_AND_AFTER
我们知道组复制是基于paxos一致性算法保证了最终一致性。但是有时某些事务需要强一致,我们必须确保事务提交成功后,其修改在其他节点(非事务执行节点)上也能马上看到;亦或者在读写分离情况下,从库的只读事务能读取到最新的事务。
而mysql 8.0.14引入的事务一致性配置参数group_replication_consistency就是用于配置事务一致性位点的参数,以解决上述各种问题。下面我们首先简单说明一下组复制原理,然后一致性参数各选项的含义,以及实现。
二、选项分析
2.1 组复制原理
大家都知道了MySQL复制的原理,这里简单说明一下组复制的实现流程原理,主要理解组复制能更好理解事务一致性等级参数。下面是官方组复制原理图。
组复制采用了乐观事务模型,事务可以在相应的服务器上先执行而无需与其他节点协商,直到提交阶段,才发送给组内的其他节点进行冲突检测,协商是否可以提交。如果可以则提交,否则就回滚。其中协商,冲突检测,事务原子广播都是在组复制插件中完成。由于各节点最终执行的事务以及顺序都一致,从而保证了数据的最终一致性。
大致理解了组复制的原理,下面讲解group_replication_consistency各选项的含义,以及内部实现方式,为了简化作图,后续图中的commit包含上图从consensus到commit的所有操作。
2.2 各选项含义
注意:下面使用都是在单主模式下进行的
- EVENTUAL
这是参数group_replication_consistency的默认值,也是8.0.14之前的一致性方式。在该配置下,RO和RW事务都不需要等待之前的事务完成。表现如下:
会话1(主,EVENTUAL级别) | 会话2(从,EVENTUAL级别) |
---|---|
①lock table t1 read; | |
②insert into t1 values (*); //执行成功,不会产生等待 | |
③select * from t1; //执行成功,返回结果为插入前的状态 | |
④unlock tables; //执行成功。此时Primary 上的insert 执行成功 | |
⑤select * from t1; //执行成功,返回结果为插入后的状态 |
可以看到:尽管会话2锁住了表t1,对于主库t1表的修改也能进行,无影响;并且③执行查询也能成功,返回旧的结果集。内部图如下:
逻辑如下:
1、在服务器M1上开始了事务T1;
2、执行到提交阶段,将事务分发到各个节点(包括自己)
3、各节点冲突检测:如果存在冲突则回滚;如果没有冲突,M1上事务进行提交,其他节点上放入relaylog,执行提交
4、M3上,在事务T1传送到服务器之前开启了事务T2,从而事务T2在T1前被执行,从而意味着T2不能看到T1的数据变化。
- BEFORE_ON_PRIMARY_FAILOVER
该一致性级别仅仅影响新主的读写。新主在应用relaylog中积累的旧主事务之前,所有事务(RO和RW)都会被阻塞。这确保在发生切换(不管有意还是无意)时,新主上总能看到最新的值。这保证了一致性,但不可避免增加新主处理积累事务的延迟。
- BEFOR
事务在执行之前等待所有先前的事务完成。这确保了事务执行在最新的数据之上,而不管这个事务在哪个服务器上执行。该级别就是可以避免在从库上的非一致性读(包含BEFORE_ON_PRIMARY_FAILOVER级别)。实验如下:
会话1(主,EVENTUAL级别) | 会话2(从) |
---|---|
①set @@session.group_replication_consistency='BEFORE' ; | |
②lock table t1 read; | |
③insert into t1 values (*); //执行成功,不会产生等待 | |
④select * from t1; //阻塞,如果超过wait_timeout设置的时间,报错:ERROR: 3797: Error while waiting for group transactions commit on group_replication_consistency= 'BEFORE' | |
⑤unlock tables; //执行成功。此时Primary 上的insert 执行成功 | |
⑥select * from t1; //执行成功,返回结果为插入后的状态 |
实现逻辑如下:
1、事务T1的执行逻辑不变;
2、在BEFOR一致性级别下,服务器M3开始事务T2,它将发送一个消息给组内所有成员(包含自己),该信息在事务T2执行前为其提供全局序列信息。
3、当大多数节点的回复信息被收到时,M3将会获得RECEIVED_TRANSACTION_SET事务GTID集合,该集合包含T2前组中所有需要提交的事务。
4、事务T2等待RECEIVED_TRANSACTION_SET事务集合中的所有事务都完成提交,才开始执行,后续步骤与一般提交一致。从而确保了事务T2读取最新的数据。
- AFTER
RW事务需要等待其在所有节点上的变更结束,才能执行成功。“AFTER”不影响RO事务。该级别确保了但一个读写事务在本地提交成功后,后续的操作不管在哪个节点都能看到最新的数据—读写事务修改的数据(包含BEFORE_ON_PRIMARY_FAILOVER级别)。实验如下:
会话1(主,AFTER级别) | 会话2(从,EVENTUAL级别) |
---|---|
①lock table t1 read; | |
②insert into t1 values (*); //等待,测试发现即使等待时间超过“wait_timeout”的阈值,也不会报错 | |
③select * from t1; //阻塞,如果超过wait_timeout设置的时间,报错:ERROR: 3797: Error while waiting for group transactions commit on group_replication_consistency= 'BEFORE' | |
④unlock tables; //执行成功。此时Primary 上的insert 执行成功 | |
⑤select * from t1; //执行成功,返回结果为插入后的状态 |
实现逻辑如下:
1、在AFTER级别下,主库事务T1前面的操作一致,只是主库在完成prepare之后,引擎层提交前等待;
2、其他节点收到事务T1,然后放入relaylog顺序执行,直到prepare完成,提交之前,发送ack给所以其他成员;
3、当所有成员收到了所有ack后(M1默认已经发送过ack,因为之前就prepare完成了),才真正引擎层提交,从而其他事务可见。
4、在M3上T2事务开始时,T1正在提交,所以T2事务需要等待,直到事务T1提交完成,后续步骤与一般提交一致
- BEFORE_AND_AFTER
这个级别就是包含了BEFOR与AFTER。事务开始之前需等待先前事务提交,事务提交之前需等待所有节点都完成。整个实现逻辑如下:
2.3 BEFOR VS AFTER
我们知道,AFTER与BEFOR两级别下,事务都需要等待,但其中有很大的不同,这里进行对比说明。
- 等待时机点不同
BEFOR级别下,是在事务执行前进行等待,对RW事务和RO事务都有影响;AFTER级别下,实在事务执行之后,引擎提交之前进行等待,仅仅对RW事务有影响。
- 等待范围不同
先看如下实验:
会话1(主,默认EVENTUAL) | 会话2(从,默认EVENTUAL) |
---|---|
set @@session.group_replication_consistency ='AFTER' ; | |
lock table t1 read; | |
insert into t1 values (*); //等待 | |
select * from t2; //执行成功 | |
set @@session.group_replication_consistency ='BEFOR' ; | |
select * from t2; //阻塞 | |
unlock tables; //执行成功。此时Primary 上的insert 执行成功 |
从上述实验可以看出:一旦事务在应用队列中,在BEFOR隔离级别下,需等待之前事务都提交,不管是否是操作同一张表或者记录。
- 影响范围不同
BEFOR级别下,影响的仅仅是本成员中事务本身,也就是他并不需要与其他成员协调,仅仅影响本事务
AFTER级别下,需要其他事务等待该事物的提交,该影响是整个组复制集群。
- 查看等待信息
查看BEFOR级别下,造成事务等待,可以通过如下命令查看:
SELECT * FROM information_schema.processlist
WHERE state='Executing hook on transaction begin.';
查看AFTER级别下,找出事务等待,可以通过如下命令查看:
SELECT * FROM information_schema.processlist
WHERE state='waiting for handler commit';
更多信息可以
SELECT *, TIME_TO_SEC(TIMEDIFF(now(),trx_started)) lock_time_sec
FROM information_schema.innodb_trx JOIN information_schema.processlist
ON processlist.ID=innodb_trx.trx_mysql_thread_id
WHERE state='waiting for handler commit' ORDER BY trx_started\G
三、总结
更为准确的准确对数据一致性进行控制固然很好,但是也存在一定风险。比如如果设置了全局AFTER级别,因为“AFTER”很有可能导致,当Primary上有大量的变更、DDL、备份等操作时,commit被延迟,并且延迟可能会导致独有的业务全部需要连接到Primary。