本文是我最初于2017年发表在我的个人微信公众号里面,现发布在这里。
1.概述
TDSQL XA的全局事务(global transaction)就是用户客户端连接到TDSQL XA分布式数据库系统后发起和执行的事务,也就是TDSQL XA处理的分布式事务。一个全局事务可能会写入数据到多个后端mysql 数据库实例,每个实例上面的本地事务都是这个全局事务的事务分支(transaction branch)。客户端发起全局事务提交时,运行在TDSQL XA的网关模块中的全局事务管理器会控制该事务访问的所有后端mysql数据库实例完成两阶段提交。
全局事务执行过程中可能发生两类死锁,第一类就是每个mysql的innodb(本文只讨论MySQL使用innodb存储引擎的情形,下同)实例内部产生的死锁,innodb现有的死锁检测和处理机制就可以处理这样的死锁;第二类就是全局死锁,这是单一的innodb实例无法解除的死锁,也就是本文要讨论的主要内容。
2.全局死锁示例
GT1和GT2分别在set1上更新数据行R1,在set2上更新数据行R2,事务分支:
GT1:
{
T11: update t1 set t1.age=11 where pk=1,
T12: update t1 set t1.age=22 where pk=2;
}
GT2:
{
T21: update t1 set t1.age=22 where pk=2,
T22: update t1 set t1.age=11 where pk=1
}
执行流程是GT1.T11在set1上执行,同时GT2.T21在set2上面执行,然后GT1.T12在set2上面执行被GT2.T21阻塞,GT2.T22在set1上面执行也被GT1.T11阻塞,此时的等待关系:
T11锁住R1, T22等待R1行锁:T22->T11,即GT2->GT1
T21锁住R2, T12等待R2行锁:T12->T21,即GT1->GT2
对于set1或者set2上面的innodb来说,它们都认为没有死锁发生,因为set1上面T22在等待T11的行锁,没有环路;set2上面T21在等待T22的行锁,也没有环路。但是全局来看,GT1和GT2确实产生了环路等待,并且每个事务分支在结束前都不会释放其已经持有的行锁,因而构成了QQ号卖号全局死锁。这种全局死锁是任何一个TDSQL XA的MySQL实例无法解除的,必须在TDSQL XA的TM(事务管理器)也就是网关中来解除。
3.全局死锁的检测和解除
与innodb或者其他事务存储引擎的全局死锁检测机制都类似,在TDSQL XA的网关中,我们使用定期检测结合语句超时触发检测。在网关中触发全局死锁检测后,全局死锁检测的具体做法是:
- 从每个后端set上查询 innodb_trx(加列xid) 和innodb_lock_waits表,得到每个set上面的事务分支的等待关系图。由于这些事务分支各自所属的全局事务也有相同的等待关系,所以可以得到每个set上的全局事务等待关系图。
- 合并全局事务的等待关系图得到GTG。
- 在GTG中寻找环路,kill掉环路当中某个连接,给它的客户端返回死锁错误。
在原始的MySQL 中, information_schema.innodb_trx表并没有XA事务ID列,但是为了第#1步从每个set的事务分支等待关系推出全局事务等待关系,就必须修改mysql和innodb的代码给这个表加上这一列。另外还需要对该表做其他一些改造方可正确完成全局死锁检测。
4.结论
全局死锁检测机制是TDSQL XA的重要组成部分:实际测试中可以看出没有全局死锁检测的话TDSQL XA在大量并发负载的情况下很容易产生很多因为全局死锁而阻塞的事务。由于TDSQL XA并不会非常频繁地执行死锁检测,其运行开销可以忽略不计。