一、前言
为什么会有分布式事务?无论是数据量导致的分库,还是现在微服务盛行的场景都是他出现的原因。
关于分布式事务,想必大多数人头脑中第一反应就是XA、2PC、3PC、TCC、MQ、Seata。
那么这些名词究竟表示什么,下面逐个说明。
二、什么是XA
XA(eXtended Architecture)是指由X/Open 组织提出的分布式事务处理的规范,他是一个规范或者说是协议,定义了事务协调者、事务参与者,和事务发起者(应用程序)。
请记住,它就是一个规范或者说协议,并不是一个真正的解决方案。
XA定义了规范,那么2PC和3PC就是他的具体实现方式。
三、2pc
2PC(two-phase commit),也叫做二阶段提交,分为准备阶段和执行阶段两个阶段,是一个非常经典的强一致。
2PC 由两类节点组成,一个协调者节点(coordinator)和N个参与者节点(partcipant)。
1、准备阶段
协调者向所有的参与者发送prepare请求,询问是否可以执行事务,等待各个参与者的响应。
这个阶段可以认为参与者只是执行了事务的SQL语句,但是还没有提交。
如果参与者执行成功了就返回YES,否则返回NO。
2、执行阶段
执行阶段就是真正的事务提交的阶段,但是要考虑到失败的情况。
如果所有的参与者都返回YES,那么就协调者会发送commit命令给所有参与者,参与者收到commit命令之后提交事务。
但只要有一个参与者在准备阶段返回的是NO的话,协调者就会发送rollback命令,然后所有参与者执行回滚的操作。
3、2PC存在的潜在问题?
(1)性能问题
从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
(2)协调者单点故障问题。
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。
(3)数据不一致问题。
在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。
既然两阶段提交有以上问题,那么有没有其他的方案来解决呢?基于2PC,有人又提出来3PC。
四、3pc
3PC是基于2PC存在的问题,而提出的新方案,它把整个流程分成了CanCommit、PreCommit、DoCommit三个步骤,相比2PC,增加的就是CanCommit阶段。
在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit命令,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。
1、CanCommit阶段
在该阶段中,协调者会向所有参与者发送请求,该阶段存在意义在于先断定是否所有参与者都能够正常提供服务。
2、PreCommit阶段
这个阶段就等同于2PC的准备阶段了,发送precommit命令,然后去执行SQL事务(执行但不提交),执行成功就返回YES,反之返回NO。
注意在3PC中,这个地方与2PC的区别在于参与者有了超时机制,如果参与者超时未收到doCommit命令的话,将会默认去提交事务。
3、DoCommit阶段
这个阶段就等同于2PC的执行阶段了,如果上一个阶段都是收到YES的话,那么就发送doCommit命令去提交事务,反之则会发送abort命令去中断事务的执行。
相比较2PC而言,3PC对于协调者和参与者都设置了超时时间,而2PC只有协调者
才拥有超时机制。
这解决了一个什么问题呢?这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC仍然没有完全解决数据不一致的问题。
五、TCC
TCC的模式叫做Try、Confirm、Cancel,实际上也是2PC的一个变种。
TCC(Try-Confirm-Cancel)又称补偿事务。
其核心思想是:"针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)"。
它分为三个操作:
- Try阶段:主要是对业务系统做检测及资源预留
- Confirm阶段:确认执行业务操作
- Cancel阶段:取消执行业务操作
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。
简单说就是2PC和3PC实际上大多数操作都是通过MySQL来实现的,但是TCC更大程度上是通过调用各方接口实现的。举个例子比如商品服务创建商品,通知库存服务和活动服务添加对应记录,库存服务和活动服务除了要有一个添加商品记录的方法,还要有一个删除商品记录的方法,以供商品服务来进行回滚。
这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。
此外,实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。
总结起来就是开发量很大,没有人用。毕竟程序员的本身素质参差不齐,多个团队协作你也很难去约束别人按照你的规则来实现。
六、MQ最终一致性事务
基于消息队列来实现最终一致性的方案。
这里需要说明的是,MQ实现的分布式事务,其实和事务关系并不大,它所能保证的是最终该执行的都执行完毕。但是对于回滚,MQ的这种方案是不支持的。
MQ是最终一致性,而2PC、3PC、TCC是强一致性。
举例:
商品服务添加一个商品,需要同时通知库存服务、代理服务、活动服务去添加这个商品的记录。
具体的代码实现我另开了一篇文章,请查看
1、使用 confirm 机制确保消息100%投递成功
// 开启confirm模式,confirm模式下,投递消息后,RabbitMQ会异步返回是否投递成功,confirm模式不可以和事务模式同时存在
$channel->confirm_select();
// 推送消息到RabbitMQ成功的异步回调,如果消息推送成功,想做什么业务处理写在这里
$channel->set_ack_handler(function (AMQPMessage $message) {
});
// 推送消息到RabbitMQ失败的异步回调,如果消息推送失败,想做什么业务处理写在这里
$channel->set_nack_handler(function (AMQPMessage $message) {
});
这里为了避免之后消费者消费消息时可能产生的重复消费问题,我们最好在消息中添加一个唯一ID(生成方式多种多样,最简单的比如时间戳+随机数+机器码),这样之后消费者消费消息时先去缓存中查有没有消费过这个消息,如果有消费过,则不再处理并且直接ack让rabbitmq删除这条消息。如果缓存中没有这个ID,则说明没有消费过这条消息,那就先消费执行业务逻辑,执行成功后将这个ID写入缓存,然后ack确认让rabbitmq删除掉这条消息。
这里需要注意的是,建议消费者将唯一ID存到缓存中时,设置个有效期TTL,这样可以避免内存爆炸。一般设置为1-2天足以了,因为即使有失败的消息,我们的业务人员也会在1-2天内手动处理好。
2、使用ack、nack机制确保消息100%消费成功
在消费消息时,将no_ack参数设置为false,表明要我们确认,rabbitmq才可以删除消息,否则不可以删除消息
// 消费监听,在参数里将第四个参数no_ack设置为false,表示需要我这里确认,你rabbitmq才可以把消息删掉
$channel->basic_consume($queueName, '', false, false, false, false, function ($message) {
// $message->body是推送过来的消息,业务代码写在这里
// 标识取出消息后,要执行的业务是否已经执行成功
$isSuccess = true;
// 如果业务执行成功,则调用ack方法,告诉rabbitmq可以把这条消息删除了
if (true == $isSuccess) {
$message->ack();
} else {
// 如果如果业务执行失败,则调用nack方法,告诉rabbitmq不可以删除这条消息,我执行失败了
$message->nack(true);
}
});
这里,对于失败的消息,可以将消息及捕捉到的错误信息先记录到日志表,待错误排查后再重新消费
七、使用阿里开源的分布式事务框架 Seata
因为官方文档介绍的非常全面了,这里就不多比比了,如有兴趣可以查看Seata官方网址 https://seata.io/zh-cn/
八、总结
分布式事务一直是微服务的痛点,本篇从XA,讲到2pc、3pc、tcc、MQ,最后提及了已经很成熟的分布式解决方案seata框架。
至于实际工作中如何选择,个人认为选择MQ方式或者Seata分布式事务框架或许会更好,原因是开发成本低、学习成本低,可靠不容易出错。