概述
本文将主要解读 PolarDB-X 中事务部分的相关代码,着重解读事务的一生在计算节点(CN)中的关键代码:从开始、执行、到最后提交这一整个生命周期。
在阅读本文前,强烈推荐先阅读与 PolarDB-X 事务系统相关的文章:
- PolarDB-X 强一致分布式事务原理
- PolarDB-X 分布式事务的实现(一)
- PolarDB-X 分布式事务的实现(二)InnoDB CTS 扩展
- PolarDB-X 分布式事务的实现(四):跨地域事务
- 无处不在的 MySQL XA 事务
以及此前发布的 PolarDB-X SQL 的一生
事务与连接
在 PolarDB-X 的 CN 层,与事务关系密切的是连接。这是因为数据节点(DN)也具备单个 DN 内的事务能力,CN 则通过与 DN 的连接来管理 DN 上的事务,从而实现强一致的分布式事务能力。其中涉及到的连接大致如下图所示:
先简单说一下这里面涉及的一些连接。ServerConnection 类似于前端连接,大部分的 SQL 语句执行的入口都是 ServerConnection#innerExecute
。TConnection 中的 executeSQL
方法负责 SQL 语句的真正执行,也负责创建新的事务对象。TConnection
会一直引用着这个事务对象,直到事务提交或回滚。事务对象里有一个 TransactionConnectionHolder,负责管理该事务用到的所有物理连接(CN 连接 DN 的私有协议连接)。值得一提的是,ExecutionContext 作为一条逻辑 SQL 执行的上下文,也会引用这个事务对象。这样,后续执行器需要使用物理连接与 DN 通信时,就可以通过 ExecutionContext
拿到事务对象,再通过事务对象的 TransactionConnectionHolder
拿到合适的物理连接。
以上的各种连接,都会在下文继续讨论。
两个例子
接下来,我们以两个简单的例子,来说明事务的一生在 CN 的代码中是如何体现的。
测试用表:
CREATE TABLE `tb1` (
`id` int PRIMARY KEY,
`a` int
) DBPARTITION BY HASH(`id`)
先在里面插入几条数据:
INSERT INTO tb1 VALUES (0, 0), (1, 1), (2, 2), (3, 3);
测试使用的两个例子:
-- Example 1:
BEGIN;
SELECT * FROM tb1 WHERE id = 0;
UPDATE tb1 SET a = 100 WHERE id = 1;
COMMIT;
-- Example 2:
BEGIN;
SELECT * FROM tb1 WHERE id = 0;
UPDATE tb1 SET a = 101 WHERE id = 1;
UPDATE tb1 SET a = 101 WHERE id = 0;
COMMIT;
注意到例 2 只比 例 1 多修改了 id = 1 的数据。测试表是按 id 拆分的,因此 id = 0 和 id = 1 的记录会落在不同的物理分片上(假设分别为分片 0 和分片 1)。例 1 读了分片 0,写了分片 1,然后提交了事务,这将会触发我们对单分片写的“一阶段提交优化”。例 2 读了分片 0,随后写了分片 1 和 分片 0,然后提交了事务,这将会进行完整的分布式事务提交流程。这两个例子还会触发“只读连接优化”,即只有在第一次写的时候才真正开启分布式事务。
在接下来的讨论中,我们默认使用 TSO 事务策略和 RR 的隔离级别。
例 1 事务的一生
BEGIN
与 MySQL 类似,要开启一个事务,一般有两种方式。第一种方式是显式地执行 BEGIN
或 START TRANSACTION [transaction_characteristic]
,执行这两种语句,会调用 ServerConnection
中的 begin(boolean, IsolationLevel)
方法。第二种方式是执行 SET autocommit = 0
,当前 session 会隐式开启事务,这种方式会调用 ServerConnection
中的 setAutocommit(boolean, boolean)
方法。两种方式都会调用 TConnection
的 setAutoCommit
方法。这些方法都只是简单地记录了一些变量(比如 transaction_characteristic
中设定的事务相关变量),同时标记这个连接开启了事务。此时,事务对象也还没创建出来,也没有与后端连接进行任何交互。
读分片 0
在开启事务后,执行 SELECT *