解决方案:
- 通过mq: 当A成功后,发送消息到mq,B消费消息,即使失败了,也能通过mq重试,知道B也成功,最终数据一致。这里要注意发送消息最好在本地事务提交之后进行。 但是还有问题,如果本地事务成功了,消息发送失败了呢?不能保证本地事务和发送消息两个操作同时都成功,这种方式存在问题!
- 2PC–两阶段提交: 也是通过mq发送消息给B,不过发送都是事务消息,Kafka和RocketMQ支持事务消息。先发送perpare消息,等待A本地事务执行后,根据本地事务执行情况发送commit或rollback。即使本地事务执行后,发送commit或rollback失败了,rocketmq在没收到下一步操作的情况下,会回溯事务是否成功,进而设置自己的perpare消息是否可消费。也存在问题,就是A成功的情况下,B必须要成功,但是B也会存在失败的情况,这里没办法进行处理。
- AT–业务无侵入: 引入TxManager(事务管理器)来管理多个本地事务。各个本地事务提交前通知TxManager,由TxManager根据各个本地事务的情况来发出commit/rollback指令。大概思路就是写一个切面,在@Transaction的方法前执行创建事务组,通过netty等通信框架传给TxManager,然后执行本地事务代码,重写数据库连接commit方法(通过自定义数据库连接),让本地事务在commit前等待,根据本地事务执行情况,提交commit或者rollback结果到TxManager。TxManager在接收到所有本地事务的结构后,计算出commit还是rollback,发送指令到各个服务。
@Aspect
@Component
public class TransactionGroupAspect implements Order {
@Around("@annotation(xxx.TransactionGroup)")
public void invoke(ProceedingJoinPoint point) {
//1. 在TxManager创建事务组,返回事务组ID
String transactionGroupId = ...
//2. 执行本地事务
try {
//执行本地事务--@Transaction方法
point.proceed();
//提交执行成功结果,此时并没有提交,而是卡在我们重写的数据库连接的commit方法。
...send commit message
} catch (Throwable throwable) {
//发送rollback消息
...send rollback message
}
//记录本地事务的transactionGroupId,待唤醒提交线程的时候使用
transactionMap.put(transactionGroupId, 本地事务对象)
}
}
@Override
public void commit() throws SQLException {
//单独起一个线程,让TransactionGroupAspect的逻辑可以继续往下执行。。。发送消息到TxManager
new Thread(new Runnable() {
@Override
public void run() {
try{
//本地事务提交前阻塞当前线程,等待TxManager发送指令来唤醒
condition.await();
//判断TxManager指令
if (command.equals("commit")) {
//调用commit(),提交本地事务。
connection.commit();
} else {
//回滚
connection.rollback();
}
}
}
}).start();
}
//接收TxManager指令
...readFromChannel
//从本地事务集合中取出之前创建的本地事务
localTransaction = transactionMap.get(transactionGroupId);
//给本地事务指定将要执行的指令
localTransaction.setCommand(commandFromTxManager);
//唤醒之前阻塞在commit()前的线程,根据TxManager指令来提交或者回滚
localTransaction.getCondition.signal();
TCC–业务有侵入: try-confirm-cancel,也是2pc,两阶段提交的方案。try阶段,我们搞一个中间状态,比如扣款先冻结,积分搞个预增加,订单搞个未完成状态。confirm阶段,可以引入开源框架,ByteTCC、Himly、TCC-transaction等,用来感知各个事务的状态,当确认所有子事务都try成功了,就控制事务进入confirm阶段,这里要把之前的操作完成,比如完成扣款,积分增加,修改订单状态为完成。如果在try阶段有任何一个事务未能完成,比如余额不足,扣款失败导致扣款事务失败,tcc事务框架感知到后,会执行cancel阶段的操作。比如把余额恢复,积分恢复,订单修改为已取消状态。
- 先是服务调用链路依次执行 Try 逻辑。
- 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
- 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。
confirm和cancel操作如果失败TCC框架会根据活动日志,不断重试,直至成功。异步调用一般基于MQ的可靠消息达到最终一致性。
Saga–业务有侵入: 和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。
- 每个Saga由一系列sub-transaction Ti 组成
- 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果
saga定义了两种恢复策略:
backward recovery,向后恢复,补偿所有已完成的事务,如果任一子事务失败。即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。
forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。
但是saga依然存在问题,比如向前恢复子事务永远不会成功,向后恢复补偿事务失败。最终还是要人工干预。
XA:XA需要两阶段提交: prepare 和 commit.
第一阶段为 准备(prepare)阶段。即所有的参与者准备执行事务并锁住需要的资源。参与者ready时,向transaction manager报告已准备就绪。
第二阶段为提交阶段(commit)。当transaction manager确认所有参与者都ready后,向所有参与者发送commit命令。
因为XA 事务是基于两阶段提交协议的,所以需要有一个事务协调者(transaction manager)来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务协调者(transaction manager)收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个XA事务中扮演的是参与者的角色,而不是事务协调者(transaction manager)。
Alibaba Seata
Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata为用户提供了AT、TCC和XA事务模式,为用户打造一站式的分布式事务解决方案。
- 强一致性: 刚性事务 使用seata的AT模式
- 弱一致性: 柔性事务 基于BASE理论的最终一致性,使用seata的saga模式
术语:
- TC 事务协调者: 维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM 事务管理器: 定义全局事务的范围,开始全局事务、提交或回滚全局事务。
- RM 资源管理器: 管理分支事务处理的结果,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。这里一般就是我们执行分支事务的服务,即事务参与者。