事务管理是Java开发中经常遇到的问题,对于单体应用的事务管理我们都比较熟悉,Spring框架也提供了多种事务管理器(Spring JDBC、Hibernate、JPA等)的对接方式。但是对于微服务架构的应用来说,要想实现分布式事务就不容易了。
分布式事务指事务的操作位于不同的节点上,同时需要保证事务的 ACID 特性。比如在电商系统的下单场景中,库存和订单如果不在同一个节点上,就涉及到分布式事务。本文主要介绍实现分布式事务的几种方案。
两阶段提交(2PC)方案
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。两阶段分为准备阶段和提交阶段。
准备阶段
协调者询问参与者事务是否执行成功(vote request),参与者返回事务执行结果。
提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务(commit);否则,协调者发送通知让参与者回滚事务(abort)。
在准备阶段,虽然参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
两阶段提交理解起来并不复杂,不过也存在一些问题:
- 同步阻塞:所有事务参与者在等待通知的时候都处于阻塞状态,无法进行其它操作。
- 单点问题:协调者在 2PC 中起到关键作用,也存在单点问题。如果一旦在提交阶段发生故障,那么所有参与者将会一直处于等待状态。
- 数据一致性:在提交阶段,如果协调者只向部分参与者发送了 Commit 消息,此时网络突然发生异常,那么将只有部分参与者提交了事务,会造成系统数据不一致。
- 容错能力:2PC方案比较保守,任意一个参与者执行事务失败,会导致整个事务失败,容错能力太差。
Seata(Simple Extensible Autonomous Transaction Architecture) 是阿里开源的分布式事务框架,就是属于二阶段提交模式。经常与Spring Cloud Alibaba 的服务注册中心Nacos结合使用。
补偿事务(TCC)方案
TCC (Try-Confirm-Cancel)的核心思想是:针对每个操作,都要有与之对应的确认(Confirm)和补偿(Cancel)操作。TCC共分为三个阶段:
- Try 阶段:完成所有业务的一致性检查,并预留资源。
- Confirm 阶段:对业务系统做确认提交,如果Try阶段执行成功的话,默认 Confirm阶段是不会出错的。
- Cancel 阶段:如果业务执行失败,取消执行的业务,并释放预留资源。
假如 A 要向 B 转账,那么:
- 在 Try 阶段,先把 A 的资金冻结起来。
- 在 Confirm 阶段,执行转账操作,如果转账成功进行解冻。
- 在Cancel阶段,如果转账失败,那么将A的资金进行解冻。。
相比2PC来说,TCC方案要更加乐观一些。不过数据一致性方面要比2PC也要差一些,在Confirm和Cancel阶段,都是有失败的可能的。TCC的性能要比2PC方案更优,因此更适合于互联网高并发场景。
本地消息表方案
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地数据库的事务机制来保证在对这两个表的操作满足事务特性,并且使用消息队列来保证最终一致性。
- A节点完成写业务数据的操作之后,向本地消息表发送一个消息,本地数据库事务保证写消息操作和写业务数据操作一并完成。
- 之后A节点将该消息转发到消息队列中,如果转发成功,则将消息从本地消息表中删除,否则继续重发。
- B节点从消息队列中读取到这个消息后,开始执行消息中的写业务数据的操作。
这个方案利用数据库的本地事务机制,避免了分布式事务,实现了最终一致性。不过需要将本地消息表耦合到业务系统中。
MQ 事务消息方案
有一些MQ产品是支持事务消息的,比如阿里的RocketMQ,支持事务消息的方式类似于二阶段提交。思路大致如下:
- 首先A节点发送Prepared消息到消息集群,会拿到消息的地址。
- 然后A节点执行本地事务。
- 节点提交确认消息发送请求,通过之前拿到的消息地址去访问消息,并修改状态。
- 消息集群收到消息发送确认以后,接下来就是推送消息给接收端进行消费。
如果确认消息发送失败,RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,就会向消息发送者确认,事务是否已经执行成功。
接下来RocketMQ会根据发送端设置的策略,决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务执行同时成功或同时失败。
消费端消费时可能存在消费超时或者消费失败的情况。解决超时问题的思路就是一直重试,直到消费端消费成功为止。可能会出现消息重复的情况,需要消费端进行去重处理。
如果出现消费失败,那么可能只能通过人工解决,不过出现这种情况的可能性也是极低的。
基于MQ的方案实现了最终一致性,而且不需要依赖本地数据库事务。不过比较挑MQ产品,很多应用较多的MQ产品,比如RabbitMQ 和 Kafka 都不支持。
我会持续更新关于物联网、云原生以及数字科技方面的文章,用简单的语言描述复杂的技术,也会偶尔发表一下对IT产业的看法,欢迎大家关注、转发和评论,谢谢。