An Introduction to Distributed Transaction

Introduction

传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用ACID Transactions。

为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性。


分布式事务

分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)

在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确的知道其他节点中的事务执行情况。所以从理论上讲,两台机器理论上无法达到一致的状态。

如果想让分布式部署的多台机器中的数据保持一致性,那么就要保证在所有节点的数据写操作,要不全部都执行,要么全部的都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果。所以他也就不知道本次事务到底应该commit还是roolback。所以,常规的解决办法就是引入一个“协调者”的组件来统一调度所有分布式节点的执行。


微服务的困境

首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如Neo4j;基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。可见在微服务架构中已经不能选择分布式事务了。

依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中应选择满足最终一致性。如果选择了最终一致性,就要保证到达最终一致性的这段时间要在用户可接受的范围之内。

最终一致性:指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。


X/Open DTP Model


What’s DTP Model

X/Open DTP(Distributed Transaction Process)是一个分布式事务模型。这个模型主要使用了2PC(Two-Phase-Commit)来保证分布式事务的完整性。

The X/Open Distributed Transaction Processing (DTP) model is a software architecture that allows multiple application programs to share resources provided by multiple resource managers, and allows their work to be coordinated into global transactions.

DTP目前最新的版本是V3,DTP模型的组成:

  • 应用程序(Application Program ,简称AP):即业务层。哪些操作属于一个事务,就是AP定义的。。
  • 资源管理器(Resource Manager,简称RM):一般是数据库,也可以是其他的资源管理器,如消息队列(如JMS数据源),文件系统等。
  • 事务管理器(Transaction Manager ,简称TM):接收AP的事务请求,对全局事务进行管理,管理事务分支状态,协调RM的处理,通知RM哪些操作属于哪些全局事务以及事务分支等等。这个也是整个事务调度模型的核心部分,一般是作为交易中间件。
  • 通信资源管理器(Communication Resource Manager,简称CRM):控制一个TM域(TM domain)内或者跨TM域的分布式应用之间的通信。
  • 通信协议(Communication Protocol,简称CP):提供CRM提供的分布式应用节点之间的底层通信服务。

an Application Program (AP), which defines transaction boundaries and specifies actions that constitute a transaction
Resource Managers (RMs) such as databases or file access systems, which provide access to resources
a Transaction Manager (TM), which assigns identifiers to transactions, monitors their progress, and takes responsibility for transaction completion and for coordinating failure recovery.
Communication Resource Managers (CRMs), which control communication between distributed applications within or across TM domains.
a communication protocol(CP), which provides the underlying communication services used by distributed applications and supported by CRMs.
Java中的javax.transaction.xa.XAResource定义了XA接口,它依赖数据库厂商对jdbc-driver的具体实现。


全局的DTP - Global DTP

  • 全局事务:对于一次性操作多个资源管理器的事务。
  • 分支事务:在全局事务中,某一个资源管理器有自己独立的任务,这些任务的集合作为这个资源管理器的分支任务。

AP可以和TM以及RM通信,TM和RM互相之间可以通信,DTP模型里面定义了XA接口,TM和RM通过XA接口进行双向通信,例如,TM通知RM提交事务或者回滚事务,RM把提交结果通知给TM。AP和RM之间则通过RM提供的Native API 进行资源控制,这个没有进行约API和规范,各个厂商自己实现自己的资源控制,比如Oracle的数据库驱动程序。


跨域的全局DTP

如果分布式事务需要跨多个应用,那么每个Model实例中,还需要额外的加入一个通信资源管理器CRM。CRM负责在多个事务域之间进行协调和沟通。

CRM作为多个Model实例之间通信的桥梁:

  • 基本的通信能力:从这个角度,可以将CRM类比为RPC框架,Model实例之间通过RPC调用实现彼此的通信。这一点体现在AP、CRM之间的连线。
  • 事务传播能力:与传统RPC框架不同的是,CRM底层采用OSI TP通信服务,因此CRM具备事务传播能力。这一点体现TM、CRM之间的连线。

    OSI TP:Open Systems Interconnection — Distributed Transaction Processing


XA规范

一个DTP Model Instance,至少有3个组成部分:AP、RMs、TM。

The subject of this X/Open specification is interface (3) in the diagram above, the XA interface by which TMs and RMs interact.
XA规范的最主要的作用是,就是定义了RM-TM的交互接口。

  • 2PC是在OSI TP标准中提出的。
  • 在DTP Model中,指定了全局事务的提交要使用2PC协议。
  • XA规范只是定义了2PC协议中需要使用到的接口,即上面提到的RM-TM交互的接口(因为2PC过程中的参与方只有TM和RMs)。

TM Domain

一个TM Domain中由一个或者多个Model实例组成,这些Model实例使用的都是同一个TM,但是操作的RMs各不相同,由TM来统一协调这些Model实例共同参与形成的全局事务(global transaction)。

TM Domain只是列出了最终参与到一个全局事务中,有哪些Model实例,并不关心这些Model实例之间的关系。这就好比,有一个班级,我们只是想知道这个班级中每位同学的名字,但是并不是关心谁是班长、谁是学习委员等。

不过显然的,当一个TM Domain中存在多个Model实例时,Model实例彼此之间存在一定的层级调用关系。这就是全局事务的树形结构

发起分布式事务的Model实例称之为root节点,或者称之为事务的发起者,其他的Model实例可以统称为事务的参与者。事务发起者负责开启整个全局事务,事务参与者各自负责执行自己的事务分支。

而从Model实例之间的相互调用关系来说,调用方称之为上游节点(Superior Node),被调用方称之为下游节点(Subordinate Node)。


2PC - Two Phase Commitment Protocol

2PC协议经常被用来实现分布式事务。一般分为协调器和若干事务执行者两种角色,这里的事务执行者就是具体的数据库,抽象点可以说是控制数据库的程序。协调器可以独立运行在一台机器上,也可以和事务执行器在同一台机器上。

2PC协议主要分为两个阶段:

  • 准备阶段(投票阶段)
  • 提交阶段(执行阶段)

2PC、Paxos和Raft协议之间的区别

  • 2PC协议用于保证多个数据分片上的操作的原子性。这些数据分片可能分布在不同的服务器上,2PC协议保证多台服务器上的操作要么全部成功,要么全部失败。
  • Raft协议和Paxos协议都是用于保证同一个数据分片的多个副本之间的数据一致性。当这些副本分布到不同的数据中心时,这个需求尤其强烈。
  • Raft协议比paxos的优点是容易理解,容易实现。它强化了leader的地位,把整个协议可以清楚的分割成两个部分,并利用日志的连续性做了一些简化:(1)Leader在时。由Leader向Follower同步日志;(2)Leader挂掉了,选一个新Leader的Leader选举算法。

Phase - 阶段说明

Phase 1 提交事务请求(投票阶段)

  1. 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
  2. 执行事务:各参与者节点执行事务操作,并将Undo和Redo信息计入事务日志中。
  3. 各参与者向协调者反馈事务询问的响应:如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。

Phase 2 执行事务提交(执行阶段)

如果所有参与者的反馈都是Yes响应,那么:

  1. 发送提交请求:协调者向所有参与者节点发出Commit请求。
  2. 事务提交:参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
  3. 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送ACK信息。
  4. 完成事务:协调者接收到所有参与者反馈的ACK消息后,完成事务。

Phase 2 中断事务

如果任何一个参与者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。那么:

  1. 发送回滚请求:协调者向所有参与者节点发出Rollback请求。
  2. 事务回滚:参与者接收到rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放整个事务执行期间占用的资源。
  3. 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送ACK信息。
  4. 中断事务:协调者接收到所有参与者反馈的ACK信息后,完成事务中断。

Advantage and Disadvantage

Advantage:原理简单、实现方便

Disadvantage:同步阻塞、单点问题、数据不一致、容错机制

  • 同步阻塞:同步阻塞会极大地限制分布式系统的性能。在Phase 2提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。
  • 单点问题:一旦协调者出现问题,那么整个二阶段提交流程将无法运转,更为严重的是,如果是在阶段二中出现问题,那么其他参与者将会一直处于锁定事务资源的状态中,无法继续完成事务操作。
  • 数据不一致:在阶段二,当协调者向所有参与者发送commit请求之后,发生了局部网络异常或协调者在尚未发完commit请求之前自身发生了崩溃,导致最终只有部分参与者接收到了commit请求,于是部分参与者执行事务提交,而没收到commit请求的参与者则无法进行事务提交,于是整个分布式系统出现了数据不一致性现象。
  • 容错机制:如果参与者在与协调者通信期间出现故障,协调者只能靠超时机制来判断是否需要中断事务,这个策略比较保守,需要更为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。

Phase 2无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

Example

假设想从支付宝里转10000元到余额宝

  1. 首先我们的应用程序发起一个请求到协调器, 然后由控制器来保证分布式事务。
  2. 准备凭证阶段
    1. 协调器先将消息写到本地日志。
    2. 向所有的参与者发起消息。以支付宝转账到余额宝为例,协调器给A的prepare消息是通知支付宝数据库相应账目扣款10000,协调器给B的prepare消息是通知余额宝数据库相应账目增加10000。
  3. 参与者收到消息后,执行具体本机事务,但不会进行commit,如果成功返回,不成功返回。同理,返回前都应把要返回的消息写到日志里,当作凭证。
  4. 协调器收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。

协调器或参与者把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。
举个例子,比如某个参与者从故障中恢复后,先检查本机的日志,如果已收到,则提交,如果则回滚。如果是,则再向控制器询问一下,确定下一步。如果什么都没有,则很可能在阶段就崩溃了,因此需要回滚。


3PC - Three Phase Commitment Protocol

对比2PC,3PC的变化:

  1. 引入超时机制。同时在协调者和参与者中都引入超时机制。
  2. 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

3PC解决了2PC的阻塞问题,但仍然没有解决出现数据不一致的问题。

Three-Phase Commit:

  • CanCommit
  • PreCommit
  • DoCommit

为了避免在通知所有参与者提交事务时,其中一个参与者crash不一致时,就出现了三阶段提交的方式。三阶段提交在两阶段提交的基础上增加了一个preCommit的过程,当所有参与者收到preCommit后,并不执行动作,直到收到commit或超过一定时间后才完成操作。

Phase - 阶段说明

Phase 1 CanCommit

  1. 事务询问:协调者向各参与者发送CanCommit的请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
  2. 参与者向协调者反馈询问的响应:参与者收到CanCommit请求后,正常情况下,如果自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No。

Phase 2 Pre Commit

如果协调者接收到各参与者反馈都是Yes,那么执行事务预提交:

  1. 发送预提交请求:协调者向各参与者发送preCommit请求,并进入prepared阶段。
  2. 事务预提交:参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日记中。
  3. 各参与者向协调者反馈事务执行的响应:如果各参与者都成功执行了事务操作,那么反馈给协调者Ack响应,同时等待最终指令,提交commit或者终止abort。

如果任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者无法接收到所有参与者的反馈,那么就会中断事务:

  1. 发送中断请求:协调者向所有参与者发送abort请求。
  2. 中断事务:无论是收到来自协调者的abort请求,还是等待超时,参与者都中断事务。

Phase 3 Do Commit

假设协调者正常工作,接收到了所有参与者的ack响应,那么它将从预提交阶段进入提交状态:

  1. 发送提交请求:向所有参与者发送doCommit请求。
  2. 事务提交:参与者收到doCommit请求后,正式提交事务,并在完成事务提交后释放占用的资源。
  3. 反馈事务提交结果:参与者完成事务提交后,向协调者发送ACK信息。
  4. 完成事务:协调者接收到所有参与者ack信息,完成事务。

假设协调者正常工作,并且有任一参与者反馈No,或者在等待超时后无法接收所有参与者的反馈,都会中断事务:

  1. 发送中断请求:协调者向所有参与者节点发送abort请求。
  2. 事务回滚:参与者接收到abort请求后,利用undo日志执行事务回滚,并在完成事务回滚后释放占用的资源。
  3. 反馈事务提交结果:参与者在完成事务回滚之后,向协调者发送ACK信息。
  4. 中断事务:协调者接收到所有参与者反馈的ack信息后,中断事务。

在DoCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。

其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)。

所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。

Advantage and Disadvantage

Advantage:降低参与者阻塞范围,并能够在出现单点故障后继续达成一致。

Disadvantage:在DoCommit阶段如果出现网络分区,协调者无法与参与者正常通信,参与者依然会进行事务提交,造成数据不一致。

J2EE 分布式事务

Java事务编程接口(JTA:Java Transaction API)和 Java事务服务(JTS;Java Transaction Service)为J2EE平台提供了分布式事务服务。

JTA定义了一套接口,其中约定了几种主要的角色:TransactionManager、UserTransaction、Transaction、XAResource,并定义了这些角色之间需要遵守的规范,如Transaction的委托给TransactionManager等。

JTS也是一组规范,上面提到JTA中需要角色之间的交互,那应该如何交互?JTS就是约定了交互细节的规范。

总体上来说JTA更多的是从框架的角度来约定程序角色的接口,而JTS则是从具体实现的角度来约定程序角色之间的接口,两者各司其职。

JTA

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public void transferAccount() { 
UserTransaction userTx = null;
Connection connA = null;
Statement stmtA = null;

Connection connB = null;
Statement stmtB = null;

try{
// 获得 Transaction 管理对象
userTx = (UserTransaction)getContext().lookup("java:comp/UserTransaction");
// 从数据库 A 中取得数据库连接
connA = getDataSourceA().getConnection();

// 从数据库 B 中取得数据库连接
connB = getDataSourceB().getConnection();

// 启动事务
userTx.begin();

// 将 A 账户中的金额减少 500
stmtA = connA.createStatement();
stmtA.execute("update t_account set amount = amount - 500 where account_id = 'A'");

// 将 B 账户中的金额增加 500
stmtB = connB.createStatement();
stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'");

// 提交事务
userTx.commit();
// 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)
} catch(SQLException sqle){
try{
// 发生异常,回滚在本事务中的操纵
userTx.rollback();
// 事务回滚:转账的两步操作完全撤销
//( 数据库 A 和数据库 B 中的数据更新被同时撤销)
stmt.close();
conn.close();
}catch(Exception ignore){

}
sqle.printStackTrace();

} catch(Exception ne){
e.printStackTrace();
}
}

Reference

https://yq.aliyun.com/articles/582282
http://xiaorui.cc/2016/02/25/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A42pc/
https://my.oschina.net/fileoptions/blog/886967#comments
https://segmentfault.com/a/1190000004474543
浅谈分布式事务控制在银行应用的实现: https://www.ctolib.com/topics-117188.html