在Group Replication 架构中,一个事务在某节点执行,commit之前,会把该事务的所产生的写集(writeset),交给group replication 去校验,看该事务是否与其它节点执行的事务相冲突,如果不冲突,本节点就会commit返回,其余节点就会把该事务的binlog event写入relay log,由applier线程应用。注意,只有多主模式(multi primary)才需要做冲突校验,对于单主模式(single primary)则没有必要,因为不可能冲突!
那么group replication 是如何校验的呢? 事务间的冲突又是如何确定的呢?
这个过程相当神秘,其实理解了又觉得其非常简单,但理解的过程很不简单!
首先我们来看这个writeset 是个什么东西。所谓写集(writeset)就是就是一个事务执行所影响的具体数据行(用主键hash的值来标识),以及执行这个事务的时候,数据库的版本快照!又一个新概念,什么是数据库的版本快照? 其实就是当前数据库已经执行了(commit)哪些事务,说白了就是gtid_executed的值。这样就好理解了。
假如我们有一个事务T1,产生了如下WriteSet
pk_v1 -- UUID:1-9
pk_v2 -- UUID:1-9
这说明事务修改了两条数据, 执行节点的数据库快照是UUID:1-9.那么这样一个WriteSet发送到group replication是如何被校验的呢?
原来在group replicatioin 的校验模块中维护了一个校验集合,使用Certification_info来管理。Certification_info是一个map对象,它里面存的也是数据条目和数据库版本的映射。与WriteSet是一样的。源码中是这样定义的:
typedef std::unordered_map<std::string, Gtid_set_ref *> Certification_info;
一旦某个事务校验通过,它产生的写集就会写入到这个校验集合(Certification_info)中。
假如Certification_info当前存在的校验快照是这样的:
pk_v1 -- UUID:1-8
pk_v2 -- UUID:1-8
...
那么事务T1的校验就会被通过。因为对于第一数据(pk_v1)来说,校验集合中的版本是UUID:1-8 (CV),而事务T1中的版本是UUID:1-9(DV), CV 是DV的子集,对于第二条数据(pk_v2)也是这样。
通过之后校验模块还要判断这个事务T1有没有GTID,如果session 中变量gtid_next=AUTOMATIC,那么它是没有GTID的。这就要为其分配一个,因为它在是在版本UUID:1-9的基础上执行的,所以给他分配下一个数字(GNO)10(当然不会简单的是这样,还有一些结合group_replication_gtid_assignment_block_size的逻辑判断,这里为简单起见,不引入它),那么这个事务的GTID就是UUID:10。
这个事务产生的WriteSet写集也会更新的校验集合(Certification_info)中:
pk_v1 -- UUID:1-9
pk_v2 -- UUID:1-9
...
如果再有事务修改pk_v1和pk_v2只须和上一个校验成功的版本对比即可。如果上一个版本是新来事务的子集那么校验就通过,否则就拒绝。源码实现中也只是一个遍历对比。其注释还是很有帮助的 certifier.cc:630。
{
for (std::list<const char *>::iterator it = write_set->begin();
it != write_set->end(); ++it) {
Gtid_set *certified_write_set_snapshot_version =
get_certified_write_set_snapshot_version(*it);
/*
If the previous certified transaction snapshot version is not
a subset of the incoming transaction snapshot version, the current
transaction was executed on top of outdated data, so it will be
negatively certified. Otherwise, this transaction is marked
certified and goes into applier.
*/
if (certified_write_set_snapshot_version != NULL &&
!certified_write_set_snapshot_version->is_subset(snapshot_version))
goto end;
}
}
上面的注释逻辑有些绕,大家要多读几遍,反复体会。正过来理解就是:某行数据的校验就是看它在校验集合中的版本(CV)是不是它被修改时所属节点的数据库快照版本(DV)的子集。如果是,就校验通过,否则拒绝。
在源码的校验方法(rpl_gno Certifier::certify)中,真正做校验的就上面那点儿代码。后面的都是校验以后的处理,比如产生GTID, 将新的写集更新到校验结合中等等。感兴趣的同行可以自己去看。
随着事务的增加,这个校验集合会不会越来越大呢? 会的。但不用担心,有单独的算法定时清理。那么什么样的数据可以被清理掉呢?
一旦事务在集群中所有节点都执行完成,那么这个事务的写集就可以从校验集合中清理掉了。它不会再影响任何事务的校验。group replication 各节点会定时将自己已经commit的事务信息发送到集群,然后汇集它们做一个交集,就是在所有节点都commit的事务集合。这个集合可以通过replication_group_member_stats表查到。
mysql> select * from replication_group_member_stats\G
*************************** 1. row ***************************
CHANNEL_NAME: group_replication_applier
VIEW_ID: 15295588114993449:10
MEMBER_ID: ec5e51ae-68a0-11e8-8571-52540017b589
COUNT_TRANSACTIONS_IN_QUEUE: 0
COUNT_TRANSACTIONS_CHECKED: 2144030
COUNT_CONFLICTS_DETECTED: 1
COUNT_TRANSACTIONS_ROWS_VALIDATING: 0
TRANSACTIONS_COMMITTED_ALL_MEMBERS: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-5439749:5446604-5446605
LAST_CONFLICT_FREE_TRANSACTION: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:5446605
COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE: 0
COUNT_TRANSACTIONS_REMOTE_APPLIED: 7
COUNT_TRANSACTIONS_LOCAL_PROPOSED: 2144027
COUNT_TRANSACTIONS_LOCAL_ROLLBACK: 0
*************************** 2. row ***************************
CHANNEL_NAME: group_replication_applier
VIEW_ID: 15295588114993449:10
MEMBER_ID: ec725fcd-692c-11e8-bb5f-525400f9f72b
COUNT_TRANSACTIONS_IN_QUEUE: 0
COUNT_TRANSACTIONS_CHECKED: 2
COUNT_CONFLICTS_DETECTED: 0
COUNT_TRANSACTIONS_ROWS_VALIDATING: 0
TRANSACTIONS_COMMITTED_ALL_MEMBERS: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-5439749:5446604-5446605
LAST_CONFLICT_FREE_TRANSACTION: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:5446605
COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE: 0
COUNT_TRANSACTIONS_REMOTE_APPLIED: 1
COUNT_TRANSACTIONS_LOCAL_PROPOSED: 1
COUNT_TRANSACTIONS_LOCAL_ROLLBACK: 0
结果集中有一个字段TRANSACTIONS_COMMITTED_ALL_MEMBERS,就是当前各节点均已执行的事务集合(stable set)。由这个结合再与校验集合Certification_info中的个条目做交集,如果校验结合中某条目是TRANSACTIONS_COMMITTED_ALL_MEMBERS的子集。那么此条目就可以被清理。源码中实现的方法:void Certifier::garbage_collect()
{
DBUG_ENTER("Certifier::garbage_collect");
DBUG_EXECUTE_IF("group_replication_do_not_clear_certification_database",
{ DBUG_VOID_RETURN; };);
mysql_mutex_lock(&LOCK_certification_info);
/*
When a transaction "t" is applied to all group members and for all
ongoing, i.e., not yet committed or aborted transactions,
"t" was already committed when they executed (thus "t"
precedes them), then "t" is stable and can be removed from
the certification info.
*/
Certification_info::iterator it = certification_info.begin();
stable_gtid_set_lock->wrlock();
while (it != certification_info.end()) {
if (it->second->is_subset(stable_gtid_set)) {
if (it->second->unlink() == 0) delete it->second;
certification_info.erase(it++);
} else
++it;
}
stable_gtid_set_lock->unlock();
更多信息还请阅读源码。这部分实现逻辑确实比较绕。描述起来也很困难。通过查阅文档,再结合源码实现,特别是源码的注释,反复体会方能理解。
参考资料:
WL#6833
阅读原文