分布式事务(Distributed Transactions)概述
分布式事务是分布式领域必须要面对的问题,同时也是衡量一个分布式系统成熟度的重要指标。那么什么是分布式事务,哪些场景会涉及到分布式事务,如何实现分布式事务?本文将重点讨论以上问题。
分布式事务定义
分布式事务来源于 数据库事务。如果说数据库事务保证了单数据库的ACID特性,那么分布式事务则是保证了分布式系统的ACID特性。针对分布式事务,业内并没有一个统一的概念,但是不同厂商对分布式事务的定义是相似的。如维基百科中分布式事务的定义是:“分布式事务是包含两个或多个跨网络主机的数据库事务。通常,主机提供事务性资源,而事务管理器负责创建和管理包含针对此类资源的所有操作的全局事务。与数据库事务一样,分布式事务必须具有所有四个 ACID(原子性、一致性、隔离性、持久性)属性,其中原子性保证一组跨网络的操作全部执行成功或全部执行失败。” 原文如下:
A distributed transaction is a database transaction in which two or more network hosts are involved.
Usually, hosts provide transactional resources, while the transaction manager is responsible for creating and managing a global transaction that encompasses all operations against such resources.
Distributed transactions, as any other transactions, must have all four ACID (atomicity, consistency, isolation, durability) properties, where atomicity guarantees all-or-nothing outcomes for the unit of work (operations bundle).
Oracle中分布式事务的定义(oracle对外博客中找到,并不一定代表oracle的观点)是:"分布式事务(有时也称为全局事务)是必须协调两个或多个相关事务的事务集合。构成分布式事务的事务可能位于同一数据库中,但更常见的是位于不同的数据库中,并且通常位于不同的位置。分布式事务的每个单独事务称为事务分支。"原文如下:
A distributed transaction, sometimes referred to as a global transaction, is a set of two or more related transactions that must be managed in a coordinated way.
The transactions that constitute a distributed transaction might be in the same database, but more typically are in different databases and often in different locations.
Each individual transaction of a distributed transaction is referred to as a transaction branch.
简单来说,分布式事务就是可保证分布式系统的ACID特性的事务。注意,分布式事务并不仅仅局限于存储层,还包括计算层。
在学习分布式事务时,还要注意分布式事务与其他事务的概念区分。区分概念的最好办法是对相关概念进行分类。这里参考凤凰架构一书中的划分标准,即 按照涉及的服务个数、数据源个数来划分事务。在原书中,作者将“单个服务使用单个数据源”的事务称为本地事务(Local Transaction),“单个服务使用多个数据源”的事务称为全局事务(Global Transaction)、“多个服务使用单个数据源”的事务称为共享事务(Share Transaction),“多个服务使用多个数据源”的事务称为分布式事务(Distributed Transaction)。笔者认同作者的事务划分标准,但是对划分后事务的命名,保持不一样的看法。首先,笔者认为全局事务就是分布式事务,只是全局事务仅应用在数据库领域。其次,针对"多个服务使用单个数据源”的事务,笔者认为可将其划分到“本地事务”中。对单一数据源来说,多个服务就是多个客户端,单一数据源是有能力保证事务的ACID特性的。当然,多服务对应一个数据源的架构,本身就是一种不合理的架构,这里不展开讨论。所以,笔者对事务的划分如下图所示:
同时,也要注意分布式事务与 DTP 模型中“分布式事务”的差异。DTP 模型所指的“分布式”是相对于数据源而言的,并不涉及服务,所以DTP描述的是“单个服务使用多个数据源”的场景。而分布式事务即包含“单个服务使用多个数据源”的场景,也包含“多个服务使用多个数据源”的场景。所以,在遇到分布式事务名词时,一定要注意上下文,避免不正确的理解。
分布式事务产生背景
对一个业务应用来说,起初是单库单表,但随着业务数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。随着对数据库的水平拆分,原来的单库单表拆分成数据库分片。如下图所示,分库分表之后,原来在一个数据库上就能完成的写操作,可能就会跨多个数据库,这就产生了跨数据库事务问题。
在业务发展初期,单业务系统架构能满足基本的业务需求。但是随着业务的进一步发展,系统的访问量和业务复杂程度都在快速增长,单系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合、可伸缩问题的需求越来越强烈。如下图所示,业务系统架构从单业务系统(单机架构)拆分成多业务系统(分布式架构),降低了各系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统容量的伸缩。
数据库分片(单服务多数据源场景)或多业务系统架构(多服务多数据源场景)会带来多个数据库的数据一致性维护问题。解决这个问题的本质是实现分布式事务。事务用来保证多个操作,要么全部执行成功,要么全部失败。本地事务可以保证一个数据库上的操作,而分布式事务则是保证分布式系统中涉及多个系统的操作,要么全部执行成功,要么全部失败。
分布式事务实现
在分布式场景下,由于分区必然存在,所以根据 CAP理论,无法同时实现强一致性和高可用性。根据不同的一致性等级,可将分布式事务进一步细分为两类:刚性事务(Rigid Transactions)和柔性事务(Flexible Transactions)。刚性事务是指具备ACID特性的分布式事务。在刚性事务中,数据可以保证强一致性,可以像使用本地事务一样使用刚性事务。柔性事务是指不具备ACID特性,但遵循BASE理论的分布式事务。柔性事务放宽了一致性要求,即保证最终一致性即可。也就是说,柔性事务允许系统有中间状态。刚性事务为保证强一致性,降低了可用性的要求。相反,柔性事务位保证高可用性,降低了一致性的要求。
针对刚性事务,通常基于 XA 协议(如2PC、3PC)来实现。对于柔型事务,有 TCC(Try-Confirm-Cancel)、Saga、本地消息表、可靠消息队列、尽最大努力通知等多种实现方案。
XA 规范(XA Specification)
XA 规范是由 X/Open Group 在 1991 年推出的分布式事务的规范。XA 规范中定义了分布式事务处理模型(Distributed Transaction Processing Model, DTP Model),其模型描述如下图所示:
这个模型主要包含三个核心角色:
(a) RM (Resource Managers):资源管理器,提供数据资源的操作、管理接口,保证数据的一致性和完整性。最有代表性的就是数据库管理系统。
(b) TM (Transaction Managers):事务管理器,是一个协调者的角色,协调跨库事务关联的所有 RM 的行为。
© AP (Application Program):应用程序,按照业务规则调用 RM 接口来完成对业务模型数据的变更,当数据的变更涉及多个 RM 且要保证事务时,AP 就会通过 TM 来定义事务的边界,TM 负责协调参与事务的各个 RM 一同完成一个全局事务。
XA规范还定义了事务管理器和资源管理器之间的接口。XA接口是双向的系统接口,在一个事务管理器和一个或多个资源管理器之间形成通信桥梁。目前,Oracle、Informix、DB2、Sybase和PostgreSQL等各主流数据库都提供了对 XA 的支持。
2PC(两阶段提交)协议
XA规范只是定义了事务管理器和资源管理器之间的接口,并没有严格要求其实现。熟悉两阶段提交的同学可能发现,两阶段提交协议中定义的协调者和参与者与XA规范的事务管理器和资源管理器功能类似。这里介绍下基于2PC协议实现的分布式事务。
(1) 预备阶段(prepare)。事务管理器记录事务开始日志,并询问本地资源管理器是否可以执行提交准备操作。本地资源管理器收到指令后,评估自己的状态,尝试执行本地事务的预备操作:预留资源,为资源加锁、执行操作等,但是并不提交事务,并等待事务管理器的后续指令。如果本地资源管理器尝试失败,则告知事务管理器本阶段执行失败并回滚自己的操作,然后不再参与本次事务。在发送了否定答复并回滚了本地事务之后,本地资源管理器才能丢弃持久化的本地事务信息。事务管理器收集本地资源管理器的响应,并记录事务准备完成日志。执行过程如下图所示:
(2) 提交/回滚阶段(commit/rollback)。这一阶段会根据上阶段的协调结果发起事务的提交或者回滚操作。如果所有本地资源管理器在上一个步骤都返回执行成功,那么会执行提交操作。具体包括:1) 事务管理器记录事务 commit 日志,并向所有本地资源管理器发起事务提交指令;2) 所有的本地资源管理器收到指令后,提交事务,释放资源,并向事务管理器响应“提交完成”;3) 如果 TM 收到所有 RM 的响应,则记录事务结束日志。
如果有本地资源管理器在上一个步骤中返回执行失败或者超时没有应答,则事务管理器统一按照执行失败处理。具体包括:1) 记录事务 abort 日志,向所有本地资源管理器发送事务回滚指令;本地资源管理器收到指令后,回滚事务,释放资源,并向事务管理器响应回滚完成;3) 如果事务管理器收到所有本地资源管理器的响应,则记录事务结束日志。执行过程如下图所示:
2PC协议虽然可以保证强一致性,但是存在以下问题:
(1) 同步阻塞。在预备阶段,本地资源管理器会独占资源,并直到整个分布式事务完成后才会释放资源。这个过程中,如果有其它请求要访问这个资源,会被阻塞。
(2) 脑裂问题:在提交/回滚阶段,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分本地资源管理器接收到 commit 消息,也就是说只有部分本地资源管理器提交了事务。
3PC(三阶段提交)协议
针对 2PC 的问题,有人提出了3PC的改进方案。3PC解决了单点故障问题,并在本地资源管理器侧引入超时机制,以避免资源的长时间锁定。但是三阶段提交方案依然无法避免脑裂的异常情况出现,实际应用案例很少,感兴趣的同学可以自行找相关资料了解。
XA规范小结
基于XA规范实现的分布式事务主要用来解决单服务多数据源的场景,对多服务多数据源的场景,其实现难度较大。尽管 XA规范比较简单,且支持 XA 规范后,使用分布式事务的成本较低。但是,XA 规范也不是银弹。XA 规范因性能不理想,无法满足高并发场景。
TCC
TCC(Try-Confirm-Cancel)最早由 Pat Helland 在 2007 年发表的论文 Life beyond Distributed Transactions:an Apostate’s Opinion》中提出。TCC 本质是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认(confirm)和补偿(cancel)操作。TCC的具体含义如下:
Try 阶段:尝试执行事务,完成所有业务检查(一致性), 预留必需的业务资源。
Confirm 阶段:对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的(也应对异常进行处理)。即:只要Try成功,Confirm一定成功。Confirm 操作要求具备幂等设计,Confirm 失败后需要进行重试。
Cancel 阶段:取消执行,主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。Cancel操作满足幂等性设计。
Saga
Saga 最早由 Hector Garcaa Molrna 和 Kenneth Salem 在1987年发表的论文 SAGAS中提出。Saga 和 TCC 一样,也是一种补偿事务,但是它没有 try 阶段,而是把分布式事务看作一组本地事务构成的事务链。事务链中的每一个正向事务操作,都对应一个可逆的事务操作。Saga 事务协调器负责按照顺序执行事务链中的分支事务,分支事务执行完毕,即释放资源。如果某个分支事务失败了,则按照反方向执行事务补偿操作。
假如一个 Saga 的分布式事务链有 n 个分支事务构成,[T1,T2,…,Tn],那么该分布式事务的执行情况有三种:
(1) T1,T2,…,Tn:n 个事务全部执行成功了。
(2) T1,T2,…,Ti,Ci,…,C2,C1:执行到第 i (i<=n) 个事务的时候失败了,则按照 i->1 的顺序依次调用补偿操作。如果补偿失败了,就一直重试。补偿操作可以优化为并行执行。
(3) T1,T2,…,Ti (失败),Ti (重试),Ti (重试),…,Tn:适用于事务必须成功的场景,如果发生失败了就一直重试,不会执行补偿操作。
可靠消息队列
可靠消息队列这个方案最早由 Dan Pritchett 在 2008 年发表的论文 Base: An Acid Alternative(BASE理论的来源)中提出。可靠消息队列的核心思想是通过可靠消息队列确保事务参与方可接收该消息并处理成功。此方案基于消息中间件实现,其执行流程如下图所示:
本地消息表
本地消息表的核心思想是通过本地事务保证数据业务操作和消息的一致性。事务发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中。业务执行成功,则同时记录一条“待发送”状态的消息到本地消息表中。系统中启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并将其发送到消息系统(MQ)中。如果发送失败或者超时,则一直发送,直到发送成功后,从本地消息表中删除该记录。事务参与方在收到消息后,执行本地事务,本地事务如果执行成功,则给 MQ 系统发送 ACK 消息;如果执行失败,则不发送 ACK 消息,MQ 系统会持续推送给消息。具体执行流程如下图所示:
在实现本地消息表时,需要保证以下约束:(1) 本地事务与消息发送的原子性;(2) 事务参与方接收消息的可靠性;(3) 消息重复消费的问题。具体含义如下:
(1) 本地事务与消息发送的原子性。针对消息创建消息表,业务逻辑和消息表通过本地事务保证一致。下边是伪代码:
begin transaction;
//1.本地事务操作
//2.存储消息日志
commit transation;
这种情况下,本地数据库操作与消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。
(2) 事务参与方接收消息的可靠性。可以使用 MQ 的 ack(即消息确认)机制。事务参与方监听 MQ,如果事务参与方接收到消息并且业务处理完成后向 MQ 发送ack,此时说明事务参与方正常消费消息完成,MQ 将不再向消费者推送消息,否则 MQ 会不断重试向事务参与方发送消息。
(3) 消息重复消费的问题。由于 MQ 的消息会重复投递,所以事务参与方在消费消息时需要确保幂等性。
尽最大努力通知
尽最大努力通知方案也是一种基于 MQ 的解决方案,但是不要求 MQ 消息可靠。尽最大努力通知方案的核心思想是事务发起方通过MQ等方式,最大努力将业务处理结果通知到事务接收方。
最大努力通知方案主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知,以实现数据最终一致性,比如跨企业的系统间业务交互场景。
总结
分布式事务根据实现的一致性等级、支持的业务场景等有多种实现方案。开发者需要根据业务特点选择合适的实现方案。分布式事务会增大系统的复杂度,在非必要的情况下,尽量不要引入分布式事务。主流的分布式事务方案以开源的 seata 解决方案最为成熟。云服务场景下,各厂商也有推出自己的GTS(Global Transaction Service)服务。
参考
https://docs.oracle.com/en/database/oracle/oracle-database/12.2/jjdbc/distributed-transactions.html Distributed Transactions
https://around25.com/blog/how-to-manage-database-transactions-in-a-distributed-system/ How to manage database transactions in a distributed system
http://en.wikipedia.org/wiki/Distributed_transaction distributed transaction
https://www.cnblogs.com/wzh2010/p/15311142.html 分布式:分布式事务(CAP、两阶段提交、三阶段提交)
https://www.sofastack.tech/blog/seata-distributed-transaction-deep-dive/ Seata 分布式事务实践和开源详解
https://www.sofastack.tech/blog/sofa-meetup-3-seata-retrospect/ 分布式事务 Seata Saga 模式首秀以及三种模式详解
https://www.cnblogs.com/crazymakercircle/p/13917517.html 分布式事务
https://pdai.tech/md/arch/arch-z-transection.html 分布式锁
http://icyfenix.cn/architect-perspective/general-architecture/transaction/distributed.html 分布式事务
https://programs.wiki/wiki/distributed-transaction.html Distributed transaction
https://codingusage.com/titledistributed-transactions-theorypractice.html Distributed Transactions (Theory+Practice)
https://shardingsphere.apache.org/blog/en/material/solution/ The mixed open-source distributed transaction solution
https://xiaomi-info.github.io/2020/01/02/distributed-transaction/ 分布式事务
https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf Distributed Transaction Processing: The XA Specification
https://blog.csdn.net/qq_31960623/article/details/119392821 分布式事务模型–XA Specification
https://developer.aliyun.com/article/762770 如何选择分布式事务解决方案?
https://cn.bing.com/translator 微软必应翻译
夜雨风云: ,感谢纠正。从评论中1997的年论文,的确能够证明Dan Pritchett不是第一个提出BASE理论的人,这块我的介绍有误,需要修改下。但是,也不可否认Dan Pritchett为BASE理论的推广做了很大的贡献。 关于BASE理论早于CAP理论,本文并没有进行比较。《Base: An Acid Alternative》这篇文章关注的是在CAP理论基础上认识BASE理论,本文的发表时间是在2008年,是要晚于CAP理论的提出时间的。 此外,BASE理论的对比对象是ACID,并没有打破CAP理论。
lee372106501: 文章一开头就写错了,BASE第一次发表的时间比CAP要早 1997年,Brewer和他的学生在ACM上《Cluster-Based Scalable Network Services》文章中提出了BASE的术语概念, 包含Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)。https://people.eecs.berkeley.edu/~brewer/cs262b/TACC.pdf 1999年,Brewer第一次发表了CAP的文章 https://www.semanticscholar.org/paper/Harvest%2C-yield%2C-and-scalable-tolerant-systems-Fox-Brewer/9c9ceb29a358149e9617d103f5624f325bf08b1e?p2df 2000年,Brewer在PODO会议上进行对BASE和CAP进行了讲解 https://people.eecs.berkeley.edu/~brewer/cs262b-2004/PODC-keynote.pdf
CSDN-Ada助手: Java 技能树或许可以帮到你:https://edu.csdn.net/skill/java?utm_source=AI_act_java
夜雨风云: AI你好,这篇博客的重点是介绍Java虚拟机的垃圾回收,如果关心Java IO相关知识细节,可以移步Java IO相关技术博客。这里做一个简单的答复(咨询文心一言): Java中的IO流是用于处理输入和输出操作的机制,它提供了一种统一的方式来读取和写入数据,无论是从文件、网络连接还是内存中。Java IO流基于流的概念,将数据的输入和输出看作是一个连续的流,流的方向可以是输入(读取数据)或输出(写入数据)。Java中的IO流分为字节流和字符流两种类型,分别用于处理字节数据和字符数据。 与Java IO流相比,NIO(New IO)是Java 1.4版本引入的一个新的IO模型,它是面向缓冲区的,非阻塞的,并且可以通过选择器来模拟多线程的IO操作。NIO与IO的主要区别和联系如下: 面向流与面向缓冲:Java IO是面向流的,这意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。而NIO是面向缓冲区的,在需要时可在缓冲区中前后移动,这增加了处理过程中的灵活性。 阻塞与非阻塞:Java IO的各种流是阻塞的,这意味着当一个线程调用read()或write()时,该线程被阻塞,直到有数据可读或数据已写入。而NIO是非阻塞的,即使数据尚未准备好,它也不会阻塞线程。 单线程与多线程:Java IO是单线程的,每个连接都需要一个独立的线程进行处理。而NIO是通过选择器来模拟多线程的,选择器允许一个线程同时处理多个通道(Channel),从而提高了效率。 此外,NIO还引入了通道(Channel)和缓冲区(Buffer)这两个核心概念。通道是对原I/O包中的流的模拟,可以通过它读取和写入数据。与流不同,通道是双向的,可以用于读、写或者同时用于读写。而缓冲区是发送给通道的所有数据的中间存储地,从通道中读取的任何数据也要先读到缓冲区中。缓冲区实质上是一个数组,但它提供了对数据的结构化访问,并且还可以跟踪系统的读/写进程。 总之,Java IO流和NIO都是Java中用于处理输入和输出操作的机制,但它们在面向流与面向缓冲、阻塞与非阻塞以及单线程与多线程等方面存在明显的区别。在实际应用中,可以根据具体需求选择使用Java IO流或NIO来进行数据的读取和写入操作。
CSDN-Ada助手: Java 中的 IO 流是什么?它与 NIO 有什么区别和联系?