随着业务的快速发展、业务复杂度越来越高,传统单体应用逐渐暴露出了一些问题,例如开发效率低、可维护性差、架构扩展性差、部署不灵活、健壮性差等等。
微服务架构是一个分布式的系统,按业务进行划分为独立的服务单元,解决单体系统的不足,同时也满足越来越复杂的业务需求。每个微服务仅关注于完成一件任务并很好地完成该任务。
微服务架构的特点
微服务架构的优势非常明显,在近些年迅猛发展
- 将复杂的业务拆分成多个小的业务,能够达到更好的业务复用,有利于人员组织分工
- 服务独立部署,独立扩容,每个服务的修改和部署对其他服务没有影响
- 每个服务可以根据业务场景选取合适的编程语言和数据库
微服务有以上的优势,但是微服务也带来不少的新问题,例如
- 服务数量众多,其测试、部署、监控等都变的更加困难。
- 单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂
- 系统微服务化后,原先是一个服务内部的本地数据库事务,被拆到了多个服务,需要在分布式环境下保证事务的一致性
上述的各项问题中,1、2都可以通过近几年涌现的各项微服务技术解决,例如k8s提供了服务发现、服务治理等。因此分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。下面将深入和大家探讨微服务架构下,分布式事务的解决方案。
从本地事务到分布式事务的演变
本地事务
我们那转账作为例子,A需要转100元给B,那么需要给A的余额-100元,给B的余额+100元,单体模式下,可以通过本地事务解决
把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
数据库事务具有ACID这4个特性:
- A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
- C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
- I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
- D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。
分布式事务典型场景
银行跨行转账业务是一个典型分布式事务场景,假设A需要跨行转账给B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的正确性,只能够通过分布式事务来解决。
将服务拆分为微服务时,遇见类似需要分布式事务的场景非常多,虽然微服务最佳实践建议尽量规避分布式事务,但是在很多业务场景,分布式事务是一个绕不开的技术问题。
分布式事务方案
分布式事务模式常见的有XA、TCC、SAGA、可靠消息,下面进行简短的介绍
两阶段提交/XA
XA是由X/Open组织提出的分布式事务的规范,XA规范主要定义了(全局)事务管理器(TM)和(局部)资源管理器(RM)之间的接口。本地的数据库如mysql在XA中扮演的是RM角色
XA一共分为两阶段:
第一阶段(prepare):即所有的参与者RM准备执行事务并锁住需要的资源。参与者ready时,向TM报告已准备就绪。
第二阶段 (commit/rollback):当事务管理者(TM)确认所有参与者(RM)都ready后,向所有参与者发送commit命令。
目前主流的数据库基本都支持XA事务,包括mysql、oracle、sqlserver、postgre
一个成功完成的XA事务时序图如下:
TCC事务方案
TCC方案其实是XA提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。
事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。
一个成功完成的TCC事务时序图如下:
SAGA事务方案
SAGA最初出现在1987年Hector Garcaa-Molrna & Kenneth Salem发表的论文SAGAS里。其核心思想是将长事务拆分为多个短事务,由Saga事务协调器协调,如果每个短事务都成功提交完成,那么全局事务就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
SAGA事务模式是DTM中最常用的模式,主要是因为SAGA模式简单易用,工作量少,并且能够解决绝大部分业务的需求。
例如我们要进行一个类似于银行跨行转账的业务,将A中的30元转给B,根据Saga事务的原理,我们将整个全局事务,切分为以下服务:
- 转出(TransOut)服务,这里转出将会进行操作A-30
- 转出补偿(TransOutCompensate)服务,回滚上面的转出操作,即A+30
- 转入(TransIn)服务,转入将会进行B+30
- 转入补偿(TransInCompensate)服务,回滚上面的转入操作,即B-30
整个SAGA事务的逻辑是:
- 执行转出成功=>执行转入成功=>全局事务完成
- 如果在中间发生错误,例如转入B发生错误,则会调用已执行分支的补偿操作,即: 执行转出成功=>执行转入失败=>执行转入补偿成功=>执行转出补偿成功=>全局事务回滚完成
Saga和TCC一样,也是最终一致性事务、柔性事务。Saga的本质就是把一个长事务分隔成一个个小的事务,每个事务都包含一个执行模块和补偿模块。
Saga没有try,直接提交事务,可能出现脏读的情况,在某些对一致性要求较高的场景下,是不可接受的。
在启动一个Saga事务时,事务管理器会告诉第一个Saga参与者,也就是子事务,去执行本地事务。事务完成之后Saga的会按照执行顺序调用Saga的下一个参与的子事务。这个过程会一直持续到Saga事务执行完毕。
如果在执行子事务的过程中遇到子事务对应的本地事务失败,则Saga会按照相反的顺序执行补偿事务。
一个成功完成的SAGA事务时序图如下:
在这个图中,我们的全局事务发起人,将整个全局事务的编排信息,包括每个步骤的正向操作和反向补偿操作定义好之后,提交给服务器,服务器就会按步骤执行前面SAGA的逻辑。
如果有正向操作失败,例如账户余额不足或者账户被冻结,那么dtm会调用各分支的补偿操作,进行回滚,最后事务成功回滚。
具体而言,当一个事务涉及到多个服务时,每个服务都需要实现相应的补偿操作。如果某个服务出现了错误,那么它会调用其他服务的补偿接口,让它们执行相应的补偿操作,以保证整个事务的一致性。与基于两阶段提交的分布式事务相比,基于补偿机制的分布式事务更加灵活,但同时也更加复杂,需要更多的开发工作。
可靠消息
消息一致性方案是通过消息中间件保证上下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个本地事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。
基于可靠消息的分布式服务是一种常见的分布式系统架构,它将业务逻辑拆分成多个服务,通过消息队列来实现服务之间的异步通信,保证系统的高可用和可伸缩性。
基于可靠消息的分布式服务的核心思想是将任务放入消息队列中,由消费者来消费这些消息并执行相应的任务。当一个服务需要与其他服务通信时,它将消息发送到消息队列中,并等待消费者来处理这些消息。这样,服务之间的通信就不再是直接的调用关系,而是通过消息传递实现的。
具体而言,基于可靠消息的分布式服务需要实现以下几个步骤:
- 消息生产者发送消息。当一个服务需要与其他服务通信时,它将消息发送到消息队列中,等待消费者来处理。
- 消息队列存储消息。消息队列会将消息存储在队列中,并为每个消息生成一个唯一的ID,以便于消息的跟踪和管理。
- 消息消费者消费消息。消息队列中的消费者会从队列中取出消息,并执行相应的任务。
- 消息确认。当消息消费者成功处理完一条消息时,需要向消息队列发送确认消息,告诉消息队列这条消息已经被消费成功。
- 消息重试。如果某个消息消费者在处理消息时出现错误,那么消息队列会将该消息重新发送给另外一个消费者,直到消息被成功消费为止。
- 消息幂等性。为了保证系统的正确性和一致性,需要保证每个消息在被消费时只会被处理一次。为了实现这一点,需要对每个消息进行幂等性处理,即确保多次消费同一条消息时,只有一次消费会被认为是有效的。
基于可靠消息的分布式服务架构具有很多优点,包括高可用、可伸缩性、解耦合等,但同时也需要注意消息队列的可靠性和性能问题。为了保证系统的正确性和性能,需要对消息队列的选型和配置进行仔细的考虑和调整。
RocketMQ 提供了典型的可靠消息接口,可以参考
分布式事务开源项目
当前的分布式事务领域,有java语言的开源项目,以seata为代表。在非java领域,go语言的DTM是代表项目。DTM支持XA、TCC、SAGA、可靠消息,架构图如下
图中的各角色与XA模型中的角色模型一致,分别解释如下
AP 应用程序(定义和提交事务,当前支持go语言,即将支持nodejs、python、PHP、Rust等)
RM 资源管理器(负责管理本地事务,不限语言,只要提供了http相关的接口即可)
TM 事务管理器(DTM,协调全局事务,进行提交以及回滚)
在上述的架构图中,AP通过DTM提供的分布式事务接口,与RM和TM交互,对现有的微服务,侵入很小
另外在实际的业务中,AP和RM角色可能会有重叠,例如TCC模式下,AP可能有自己的本地事务,也会注册并调用其他事务分支。