分布式事务处理方式
分布式事务的典型处理方式
柔性事务和刚性事务
柔性事务满足BASE理论(基本可用,最终一致)。
刚性事务满足ACID理论。
在分布式事务当中主要讨论的是柔性事务的处理方式。
柔性事务分为:
- 两阶段提交型(2PC)
- 三阶段提交型(3PC)
- 补偿型(TCC、SAGA)
两阶段提交(2PC)型
两阶段提交(2-Phase Commit, 2PC)是一种比较简单的分布式一致性协议。
2PC协议中,每个事务需要一个协调者来协调各个参与者。每个事务分为两步执行。
- 阶段一: 事务请求
- 协调者向所有参与者发送事务内容,询问是否可以执行事务操作。
- 各参与者执行事务,写事务日志但不进行提交。 各参与者锁定事务相关的资源,保证事务可以正常提交。
- 各参与者向协调者返回响应,YES表示可以提交,NO表示不可以提交。若协调者收到所有参与者的YES回复,则准备进行事务提交。若有参与者回复NO或者超时,则准备回滚事务。
- 阶段二: 提交事务
- 协调者向所有参与者发送提交请求
- 参与者正式提交事务,并在完成后释放相关资源。
- 参与者向协调者回复ACK,协调者收到所有参与者的ACK后认为事务提交成功。
- 回滚事务
- 在事务请求阶段若有参与者回复NO或者超时,协调者向所有参与者发出回滚请求
- 各参与者执行事务回滚,并在完成后释放相关资源。
- 参与者向协调者回复ACK,协调者收到所有参与者的ACK后认为事务回滚成功。
2PC是一种简单的一致性协议,它存在一些问题:
- 单点服务: 若协调者突然崩溃则事务流程无法继续进行或者造成状态不一致
- 无法保证一致性: 若协调者第二阶段发送提交请求时崩溃,可能部分参与者收到COMMIT请求提交了事务,而另一部分参与者未收到请求而放弃事务造成不一致现象。
- 阻塞: 为了保证事务完成提交,各参与者在完成第一阶段事务执行后必须锁定相关资源直到正式提交,影响系统的吞吐量。
参与者在完成阶段一的事务执行后等待协调者的下一个请求,若协调者超时则可以自行放弃事务。
这种方案仍然有无法保证一致性的缺点,但并不会出现某些资料所述一直锁定资源,无法继续的情况。
三阶段提交(3PC)型
三阶段提交协议(3-Phase Commit, 3PC)进一步将事务请求分为两个阶段,可以解决2PC协议阻塞的问题但无法解决单点服务和不一致的问题。
3PC协议下事务分三步提交:
- CanCommit
- 协调者向所有参与者发送CanCommit请求
- 各参与者判断是否可以完成事务提交,但不执行事务也不锁定资源
- 各参与者根据是否可以完成事务向协调者回复YES或NO
- PreCommit
- 协调者向所有参与者发送PreCommit请求,执行事务预提交
- 各参与者执行事务,写事务日志但不进行提交。 各参与者锁定事务相关的资源,保证事务可以正常提交。
- 各参与者向协调者返回响应。若协调者收到所有参与者的YES回复,则准备进行事务提交。若有参与者回复NO或者超时,则放弃事务。
- DoCommit
- 协调者向所有参与者发送提交请求
- 参与者正式提交事务,并在完成后释放相关资源。
- 参与者向协调者回复ACK,协调者收到所有参与者的ACK后认为事务提交成功。若有参与者回复NO或者超时,则回滚事务。
- 参与者进入 PreCommit 状态后,若始终未收到协调者的 DoCommit 请求则会超时后自动执行提交。
三阶段提交协议在CanCommit阶段不锁定资源,解决了阻塞降低吞吐量的问题。
若某个参与者进入 PreCommit 后始终未收到协调者的进一步指令则会自动提交,该策略一定程度上避免协调者单点服务问题。
但是 3PC 仍然无法解决数据不一致问题。
事务补偿型
TCC型事务(Try/Confirm/Cancel)可以归为补偿型。补偿型的例子,在一个长事务( long-running )中 ,一个由两台服务器一起参与的事务,服务器A发起事务,服务器B参与事务,B的事务需要人工参与,所以处理时间可能很长。如果按照ACID的原则,要保持事务的隔离性、一致性,服务器A中发起的事务中使用到的事务资源将会被锁定,不允许其他应用访问到事务过程中的中间结果,直到整个事务被提交或者回滚。这就造成事务A中的资源被长时间锁定,系统的可用性将不可接受。
WS-BusinessActivity提供了一种基于补偿的long-running的事务处理模型。还是上面的例子,服务器A的事务如果执行顺利,那么事务A就先行提交,如果事务B也执行顺利,则事务B也提交,整个事务就算完成。但是如果事务B执行失败,事务B本身回滚,这时事务A已经被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态。这样的SAGA事务模型,是牺牲了一定的隔离性和一致性的,但是提高了long-running事务的可用性。
TCC事务
TCC 将事务提交分为 Try - Confirm - Cancel 3个操作。
- Try:预留业务资源/数据效验
- Confirm:确认执行业务操作
- Cancel:取消执行业务操作,回收资源
TCC优点:让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
TCC不足之处:
- 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
- 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
流程:
- 发起方发送
try
给所有参与者 - 参与者执行
try
, 尝试预留资源, 并返回给发起者 - 发起者接收所有参与者的返回信息
- 发起者发送
commit
/cancel
指令给参与者 - 参与者执行
commit
/cancel
, 并返回执行结果 - 发起者接受参与者的结果
例子:
购买从 广州到北京的机票, 因为购买不到直达的机票, 所以购买 广州->上海->北京, 在上海中转
广州->上海 南航
上海->北京 东航
因为不属于同一个航空公司, 所以需要分别购买
如果订票系统依次购买, 可能存在第一家购买成功, 第二家购买失败, 这事不能接受的
所以订票系统先向两家航空公司发送请求, 确定是否有足够的余票, 并让对方预留票
- 如果两边都返回预留成功, 订票系统就同时向两边发送请求, 进行确认购票
- 如果订票系统迟迟没有成功发送确认指令, 预留的票会被航空公司自动取消
- 如果任何一家航空公司返回预留失败, 订票系统就向所有航空公司发送请求, 进行取消所有已经预留的票
与 2PC 的比较
TCC | 2PC | |
---|---|---|
第一阶段 | Try: 请求原业务方预留资源 | Prepare: 询问是否可以进行提交 |
段二阶段(成功) | Confirm:确认执行 | Commit: 提交事务 |
第二阶段(失败) | Cancel: 取消执行资源操作 | Rollback: 回滚事务 |
2PC 是资源层面的, 基于数据库底层 (比如mysql的xa事务), 开发者不可感知, 无侵入性
TCC 是业务层面, 开发者可感知, 可以根据业务对事务做特定的优化
存在的问题
存在和 2PC 类似的问题
如果发起者发送 confirm
过程失败, 导致有的参与者接收到指令, 执行了comfirm
而有的参与者由于没有接收到指令, 而因为timeout
, 执行了cancel
破坏了整个系统的数据一致性
Saga
背景:
一直以来, 执行数据库事务都是使用 LLT (long lived transaction), 即跨越多个数据库事务的事务, 一次性完成所有事务, 其中不允许其他事务打断, 使用这种机制, 可以保证数据库的数据一致性
但是会带来几个问题:
- LLT 可能会涉及到大量的数据库实例, 需要对其进行封锁, 不允许其他事务进行访问, 但由于 LLT 常常需要消耗大量的时间, 可能造成其他事务长期得不到资源, 而处于阻塞状态
- 可能出现两个 LLT 各自封锁了部分实例, 但是都达不到各自执行的要求, 两方出现死锁
简述
而 Saga 模式是为了弥补 LLT 的缺陷
Saga 模式将一个长事务分割多个子事务 (saga), 然后逐一执行每一个子事务, 一旦其中一环出现了错误, 通过补偿机制, 一一回滚之前已经执行的事务
$$
T1 T2 T3...Tj...Tn\
transaction\
C1 C2 C3...Cj...Cn \
compensating\ transaction
$$
整个流程有以下情况:
- 全部成功
$$
T1 → T2 → T3 →...→...→ Tn
$$
- 中途失败后回滚
$$
T1 → T2 → T3 →...→...→ Tj → Cj →...→ C3 → C2 → C1
$$
注意点:
- Tj 和 Cj 是幂等的
- 如果 Cj 执行不成功就需要人工介入
- Tj-Cj 的结果应和 Cj - Tj 的执行结果相同
例子
假设一个人需要进行如上的飞机票预订, 订票系统需要向四个航空公司发出订票请求
如果其中任何一个环节出现了问题, 就通过补偿机制, 将之前预订的票逐一退订
与TCC的比较
- 不强制参与者实现
try
接口, 只需要可以commit
和rollback
- 不需要占有预留资源
- 理想情况下, 只需要向每个参与者发送1次请求 (TCC需要2次)