3. 原理和设计

我们要设计一个对业务无侵入的方案,所以从业务无侵入的 XA 方案来思考:

是否可以在 XA 的基础上演进,解决掉 XA 方案面临的问题呢?

3.1 如何定义一个分布式事务?

首先,很自然的,我们可以把一个分布式事务理解成一个包含了若干 分支事务全局事务全局事务 的职责是协调其下管辖的 分支事务 达成一致,要么一起成功提交,要么一起失败回滚。此外,通常 分支事务 本身就是一个满足 ACID 的 本地事务。这是我们对分布式事务结构的基本认识,与 XA 是一致的。

Global & Branch Transaction

其次,与 XA 的模型类似,我们定义 3 个组件来协议分布式事务的处理过程。

FESCAR Model

  • Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
    一个典型的分布式事务过程:

  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。

  • XID 在微服务调用链路的上下文中传播。
  • RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  • TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  • TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
    Architecture

至此,Fescar 的协议机制总体上看与 XA 是一致的。

3.2 与 XA 的差别在什么地方?

架构层次

RM in Architecture

XA 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身(通过提供支持 XA 的驱动程序来供应用使用)。

而 Fescar 的 RM 是以二方包的形式作为中间件层部署在应用程序这一侧的,不依赖与数据库本身对协议的支持,当然也不需要数据库支持 XA 协议。这点对于微服务化的架构来说是非常重要的:应用层不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。

这个设计,剥离了分布式事务方案对数据库在 协议支持 上的要求。

两阶段提交

先来看一下 XA 的 2PC 过程。

XA-2PC

无论 Phase2 的决议是 commit 还是 rollback,事务性资源的锁都要保持到 Phase2 完成才释放。

设想一个正常运行的业务,大概率是 90% 以上的事务最终应该是成功提交的,我们是否可以在 Phase1 就将本地事务提交呢?这样 90% 以上的情况下,可以省去 Phase2 持锁的时间,整体提高效率。

FESCAR-2PC

  • 分支事务中数据的 本地锁 由本地事务管理,在分支事务 Phase1 结束时释放。
  • 同时,随着本地事务结束,连接 也得以释放。
  • 分支事务中数据的 全局锁 在事务协调器侧管理,在决议 Phase2 全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁 才被持有至分支的 Phase2 结束。
    这个设计,极大地减少了分支事务对资源(数据和连接)的锁定时间,给整体并发和吞吐的提升提供了基础。

当然,你肯定会问:Phase1 即提交的情况下,Phase2 如何回滚呢?

3.3 分支事务如何提交和回滚?

首先,应用需要使用 Fescar 的 JDBC 数据源代理,也就是 Fescar 的 RM。

Data Source Proxy

Phase1:

Fescar 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。

这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。

Branch Transaction with UNDO LOG

基于这样的机制,分支的本地事务便可以在全局事务的 Phase1 提交,马上释放本地事务锁定的资源。

Phase2:

  • 如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。
    Global Commit

  • 如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
    Global Rollback

3.4 事务传播机制

XID 是一个全局事务的唯一标识,事务传播机制要做的就是把 XID 在服务调用链路中传递下去,并绑定到服务的事务上下文中,这样,服务链路中的数据库更新操作,就都会向该 XID 代表的全局事务注册分支,纳入同一个全局事务的管辖。

基于这个机制,Fescar 是可以支持任何微服务 RPC 框架的。只要在特定框架中找到可以透明传播 XID 的机制即可,比如,Dubbo 的 Filter + RpcContext。

对应到 Java EE 规范和 Spring 定义的事务传播属性,Fescar 的支持如下:

  • PROPAGATION_REQUIRED: 默认支持
  • PROPAGATION_SUPPORTS: 默认支持
  • PROPAGATION_MANDATORY:应用通过 API 来实现
  • PROPAGATION_REQUIRES_NEW:应用通过 API 来实现
  • PROPAGATION_NOT_SUPPORTED:应用通过 API 来实现
  • PROPAGATION_NEVER:应用通过 API 来实现
  • PROPAGATION_NESTED:不支持

3.5 隔离性

全局事务的隔离性是建立在分支事务的本地隔离级别基础之上的。

在数据库本地隔离级别 读已提交 或以上的前提下,Fescar 设计了由事务协调器维护的 全局写排他锁,来保证事务间的 写隔离,将全局事务默认定义在 读未提交 的隔离级别上。

我们对隔离级别的共识是:微服务场景产生的分布式事务,绝大部分应用在 读已提交 的隔离级别下工作是没有问题的。而实际上,这当中又有绝大多数的应用场景,实际上工作在 读未提交 的隔离级别下同样没有问题。

在极端场景下,应用如果需要达到全局的 读已提交,Fescar 也提供了相应的机制来达到目的。默认,Fescar 是工作在 读未提交 的隔离级别下,保证绝大多数场景的高效性。

Isolation

事务的 ACID 属性在 Fescar 中的体现是一个比较复杂的话题,我们会有专门的文章来深入分析,这里不做进一步展开。