分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用。一个应用可能被拆分成多个服务并由不同的团队进行维护。为了保证在整体上满足事务性,需要分布式事务来保证这些服务能全部失败或全部成功。
数据库进行分库分表后,需要在不同的机器上进行操作。同样是多机器上的状况。
随着分布式系统的发展,数据库的ACID四大特性,已经无法满足我们分布式事务。于是产生了以下几种理论:
简介:
CAP理论指的是在一个分布式系统中,最多只能满足C、A、P中的两个需求
同一个数据中的多个副本是否实时相同。
一致性可以简单分为最终一致性、弱一致性与强一致性。
强一致性要求在数据更新的时候,同一数据的多个副本都要实时相同,不存在一个数据已经更新而其他数据尚未更新的中间状态。
弱一致性允许数据存在中间状态,即允许某个副本数据已经更新而其他数据副本尚未更新的情况。
最终一致性是弱一致性的一种特殊形态:不要求副本数据能够实时更新,只要最终整个集群中的数据能够一致即可。
服务器在正常响应的时间内,可以返回一个可用的结果。
将同一服务分布于多个系统中,从而保证在遇到节点故障、网络分区等一系列情况时,仍然能够对外提供满足一致性或可用性的服务。除非整个网络环境都发生了故障。
**网络分区:**在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但它们的内部子网络是正常的,从而导致了整个系统环境被切分成了若干个独立的分区。
论证:
假设网络中有两个节点G1和G2,G1与G2通过网络连接实现相互通讯,此时满足分区容错性。在出现网络分区分区的情况下(不可避免),G1与G2之间的通讯通道断开了。此时客户端往G1写入数据,而G1需要将写入的数据更新到G2中,但是却因为网络问题没有更新成功,而此时一个客户端对G2发起了读取更新数据的请求。此时系统有两个选择:
在不支持分区容错率的情况下,假设只存在G1一个节点,由于没有网络的限制,G1所需要的资源来自本地或者本地计算。因此天生就具有一致性与可用性(AC)。但在集群环境中,分区是会始终存在的,因此我们更多考虑的是前面两种方案。
BASE理论是以下三种状态的缩写:
假设系统出了不可预知的故障(网络分区、节点故障),但还是能用,只是相较于正常的系统而言降低了响应的时间、损失了一定的功能。
允许系统中的数据存在中间状态,并认为中间状态不影响系统的整体可用性。
在一个时间期限内,应当保证所有副本能够达到数据一致性。
在实际工程实践中,最终一致性分为以下五种:
**因果一致性:**如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。与此同时,与节点A无因果关系的节点C的数据访问则没有这样的限制。
**读己之所写:**节点A更新一个数据后,它自身总能访问到自身更新过的最新值,而不会看到旧值。
**会话一致性:**将系统数据的访问过程定框在一个会话中。系统能够保证在同一个有效的会话中实现读己之所写的一致性。
**单调读一致性:**如果一个节点从系统中读取一个数据项的某个值后,那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。
**单调写一致性:**一个系统保证来自同一个节点的写操作被顺序执行。
核心思想是:既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
不同于ACID的强一致性模型,BASE提出通过牺牲强一致性来获得可用性,并允许数据段时间内的不一致,但最终额能够达到一致状态。
而XA XA是由X/Open组织提出的分布式事务的规范,交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。
XA规范主要定义了(全局)事务管理器™和(局部)资源管理器(RM)之间的接口。XA接口是双向的系统接口,在事务管理器(TM)以及一个或多个资源管理器(RM)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或JMS队列)。
二阶提交协议和三阶提交协议就是根据这一思想衍生出来的。可以说二阶段提交其实就是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
在一般面向用户的分布式系统中,分区容错性与可用性是我们更为关注的部分,但是在服务和数据库之间维护数据一致性是非常根本的需求,基于BASE理论,我们一般会要求分布式系统满足最终一致性。总体来说,实现最终一致性有三种模式:可靠事件模式(1.4.1、1.4.2、1.4.3)、业务补偿模式(1.4.5)、TCC模式(1.4.4)。
全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它定义了一些模型对象和对象间行为,通过这些对象和对象间行为来指导分布式事务实现。
DTP协议假设整个分布式事务有三个对象参与完成,他们分别是:
一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库。
通常把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理的对象是全局事务。
所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务,例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。 一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。
这种实现分布式事务的方式需要通过消息中间件来实现,此时该消息中间件扮演了事务协调者的角色。
假设有A和B两个系统,分别可以处理任务A和任务B。此时系统A中存在一个业务流程,需要将任务A和任务B在同一个事务中处理。A系统也可以当成与用户直接交流的上游系统,B系统可以当成不与用户直接交流的下游系统。
正常流程如下:
在5执行的过程中,commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,
另外,在系统执行完A任务与执行完B任务之间存在着一定的时间差,在这个时间差里,系统处于数据不一致的状态。但是经过短暂的处理后,系统便会实现最终一致性。
当A执行失败时:
与正常流程相比,第5步之后发生了变化。
在实际系统中,Commit与Rollback指令都可能在传输途中丢失。当出现这种情况的时候,消息中间件使用超时询问机制来保证数据的最终一致性。
超时询问机制:
在该事务中存在两处地方可能发生消息丢失:
在第一种情况,当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果:
在第二种情况,当消息中间件向B系统投递完消息后便进入阻塞等待状态,如果消息在投递时丢失或者消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。
若上述的超时询问机制在多次重试后仍然无法完成事务的正常进行,那么可能是出现了网络断开、机器宕机等需要人工干预的情况。
可以发现在上述的使用中,系统A向消息中间件投递消息采用的是异步的方式,而消息中间件向系统B投递消息采用的是同步的方式,其原因如下:
对于系统A来说,其一般直接承担着与用户交流的任务,在实时性以及并发性上有着较高的要求。因此一般采用异步通信的方式,虽然会提高消息丢失的风险,但是可以使用超时询问机制来进行弥补。而对于采用异步通信来说,没有了长时间的阻塞等待,因此系统的并发性也大大增加。
对于消息中间件向系统B投递消息来说,异步能提升系统性能,但随之会增加系统复杂度。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。另外通过BASE理论的最终一致性,消息中间件产生的时延导致事务短暂的不一致是可以接受的。
也叫最大努力通知(定期校对):
该小节承接1.4.2,主要讲述基本实现原理与基于可靠消息服务的分布式事务相同,但在事务的责任上有所偏移的另一种方案。
该方法适用于不支持事务型消息的消息中间件,将消息中间件需要实现的事务功能转移到系统AB上,该方法能够通过重试机制+定期校对实现分布式事务
在该事务中存在两处地方可能发生消息丢失:
对于第一种情况,可以在上游系统建立一张本地消息表,并将 任务处理过程 和 向本地消息表中插入消息 这两个步骤放在一个本地事务中完成。如果向本地消息表插入消息失败,那么就会触发回滚,之前的任务处理结果就会被取消。如果这两步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,如果发送失败它会返回重试。
对于第二种情况,在重试多次失败后,如果超过了重试的上限仍然投递失败,那么消息中间件不再投递该消息,而是记录在失败消息表中,消息中间件需要提供失败消息的查询接口,B系统会定期查询失败消息,并将其消费,这就是所谓的“定期校对”。另外一种方法是,由系统B维护未完成消息表和已完成消息表,消息中间件只需轮询将消息发送给系统B。系统B在收到消息后,先检查是否在已完成消息表执行过该消息,若没有则将该消息插入未完成消息表中,待执行完毕中将未完成消息表中的相应消息删除,并通知消息中间件已完成任务。
但相比于直接使用支持事务性的消息中间件,它达到数据一致性的周期较长,而且还需要在A系统中实现消息重试发布机制,以确保消息成功发布给消息中间件,在B系统实现定期校验,以确保B系统能够正确接收消息中间件锁传递的消息。这无疑增加了业务系统的开发成本,使得业务系统不够纯粹,并且这些额外的业务逻辑无疑会占用业务系统的硬件资源,从而影响性能。
如果重复投递和定期校对都不能解决问题,往往是因为系统出现了严重的错误,此时就需要人工干预。
消息中间件的作用:
支持事务:支持事务的消息中间件能够移除业务逻辑系统保证事务方面的工作,使得业务系统更加存粹。
保证幂等性:中间件要保证消息一定会到,而且尽量只会到一次。
信息交流:提供多个系统之间交流的渠道。
B系统处理失败情况:
在该篇中没有谈及到关于B系统处理失败后,整体事务应该如何回滚的问题。在实际的分布式事务应用中,比如在编排式Saga中采用补偿事务的方式来进行事务的回滚。
TCC是一个分布式的事务,主要用于多个微服务系统之间的事务。简单来说,它的产生是为了解决微服务中事务的隔离性。TCC一共分为三个阶段,分别是Try、Comfirm、Cancel。
此处假设我们正在开发一个电商系统,一共涉及到订单服务、支付服务、库存服务:
按照最简单的理解,在用户下单时,需要:
对于分布式应用来说,订单服务、支付服务、库存服务分散在不同的机器上,尽管我们可以使用单机事务来保证每个机器上满足ACID,但是在多个机器上,我们要怎么保证以上三个步骤要么一起成功,要么一起失败,它们必须是一个整体性的事务。
举例:操作2支付失败了,而其他两个操作却成功了,那商家不就亏本了?
一般来说,订单服务中的代码是这样的:
public class OrderService {
// 支付服务
@Autowired
private PayService payService;
// 库存服务
private InventoryService inventorService;
// 完成交易
public void transaction() {
// 修改订单状态
orderDao.updateStatus(OrderStatus.PAYED);
// 支付
payService.pay();
// 减少库存
inventorService.redueceStock();
}
}
在这一段代码中简单包含了一个交易流程,在用户购买商品时,先将本地订单修改为OrderStatus.UPDATING
状态,减少库存,然后支付。但是这样的话不能够保证各个服务之间的事务性,因为每一个服务的调用都会涉及到远程调用,而事务回滚在服务间是不管用的。即使减少库存里失败了,订单信息也会照常更新。
这个时候就需要用到我们的TCC分布式事务:
TCC实现阶段一:Try
首先是Try,简单来说即是尝试更新。比如,我们在更新本地订单或是减少库存时,既然服务可能会失败,那么我们干脆就不要直接更新目标字段,我们弄一个备选的字段用来存储即将要更新的值。例:在数据库中使用prepareStatus
来存储status
可能更新的值,以此来锁定数据字段,这样,在后续服务调用失败时,进入TCC第三阶段:Cancel,若后续服务调用都成功,则进入TCC第二阶段:Comfire。
RCC实现阶段二:Comfire
Comfire,即是确定执行。在各个服务都处理成功的情况下。将prepareStatus
等字段中的预备值更新或叠加到status
等字段中,实现最后的更新阶段。
RCC实现阶段三:Cancel
在Try阶段发生异常或者失败时,就得将prepareStatus
中的值更新为原本的状态。比如取消订单服务,他得提供一个OrderServiceCancel的类,在里面有一个pay()接口的Cancel逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。
TCC分布式事务只要感知到了任何一个服务的Try逻辑失败了,就会跟各个服务内的TCC分布式事务进行通信,然后调用各个服务的Cancel逻辑。
总的来说,分布式事务TCC是为了确保多个服务之间能够正常使用事务。
使用TCC事务的话。你原本的一个接口,要改造为3个逻辑,Try-Confirm-Cancel。
Saga事务模型又叫做长时间运行的事务(Long-running-transaction),它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
一阶段提交非常直白,就是从应用程序向数据库发出提交请求到数据库完成提交或回滚之后将结果返回给应用程序的过程。一阶段提交不需要**“协调者”角色,各结点之间不存在协调操作,因此其事务执行时间比两阶段提交要短,但是提交的“危险期”**是每一个事务的实际提交时间,相比于两阶段提交,一阶段提交出现在“不一致”的概率就变大了。但是我们必须注意到:只有当基础设施出现问题的时候(如网络中断,当机等),一阶段提交才可能会出现“不一致”的情况,相比它的性能优势,很多团队都会选择这一方案。
两阶段提交协议(The two-phase commit protocol,2PC)是XA用于在全局事务中协调多个资源的机制。两阶段协议遵循OSI(Open System Interconnection,开放系统互联)/DTP标准。
使用两阶段提交保证分布式事务的原子性:即所有结点要么全做要么全不做。
当commit()
请求从客户端向事务管理器发出,事务管理器开始两阶段提交过程。在第一阶段,所有的资源被轮询到,问它们是否准备好了提交作业。每个参与者可能回答“准备好(READY)”,“只读(READ_ONLY)”,或“未准备好(NOT_READY)”。如果有任意一个参与者在第一阶段响应“未准备好(NOT_READY)”,则整个事务回滚。如果所有参与者都回答“准备好(READY)”,那这些资源就在第二阶段提交。回答“只读(READ_ONLY)”的资源,则在协议的第二阶段处理中被排除掉。
在两阶段提交的过程中,可能会因为网络异常、事务管理器TM所在机器宕机等原因导致资源管理器RM无法接收到事务管理器的下一步指令,这时候资源管理器可能会使用“经验化决策”的策略,或者提交,或者回滚它自己的工作,而不受事务管理器的控制。“经验化决策”是指根据多种内部和外部因素做出智能决定的过程。当资源管理器这么做了,它会向客户端报上一个经验异常(Heuristic Exception)。
经验异常最常见的原因是第一阶段和第二阶段之间的超时情况。当通讯延迟或丢失,资源管理器或许要做出提交或回滚其工作的决定,以释放资源。
JTA暴露出的三种JTA经验异常为HeuristicRollbackException,HeuristicCommitException,以及HeuristicMixedException。我们分别用下面的场景说明之:
1. 在commit操作阶段的HeuristicRollbackException异常
客户端在XA环境下执行更新操作,向事务管理器发起提交当前事务的请求。事务管理器开启两阶段提交流程的第一阶段,随即轮询资源管理器。所有资源管理器向事务管理器报告说它们已经做好了提交事务的准备。然而,在(两阶段提交流程的)第一阶段和第二阶段之间每个资源管理器独立的做出了回滚它们已完成工作的经验性决定。当进入第二阶段,提交请求被发送到资源管理器时,因为所做的工作已经在此之前回滚了,事务管理器将会向调用者报告HeuristicRollbackException异常。
当接受到此类异常时,常用的正确处理方式是将此异常传回客户端,让客户端重新提交请求。我们不能简单的再次调用commit请求,因为对数据库产生的更新已经随回滚操作从数据库事务日志中删除了。
2. 在commit操作阶段的HeuristicCommitException异常
该异常与第一个异常类似,不同的地方是,在(两阶段提交流程的)第一阶段和第二阶段之间每个资源管理器独立的做出了提交它们已完成工作的经验性决定。
3. 在commit操作阶段的HeuristicMixedException异常
客户端在XA环境下执行更新操作,向事务管理器发起提交当前事务的请求。事务管理器开启两阶段提交流程的第一阶段,随即轮询资源管理器。所有资源管理器向事务管理器报告说它们已经做好了提交事务的准备。和第一种场景不同的是,在第一阶段和第二阶段发生的间隙,有资源管理器(例如消息队列)做出了经验性的决定提交其工作,而其他资源管理器(例如数据库)做出了回滚的经验性决定。在这种情况下,事务管理器向调用者报告HeuristicMixedException异常。
这种情况下,非常难于选择正确的后续应对方式,因为我们不知道哪些资源提交了工作,哪些资源回滚了工作。所有目标资源因此处于一种不一致的状态。因为资源管理器彼此互不干预的独立操作,就经验性决定而言,他们之间没有任何协调和通信。解决这一异常通常需要人力介入。
由于XA环境中双向通信的能力,两阶段提交变得可能。在非XA事务环境中,通信仅仅是单向的,两阶段提交没法做到,这是因为事务管理器没法接收到来自资源管理器的响应。大多数事务管理器为了优化性能,尽快释放资源的目的,用多线程处理第一阶段轮询以及第二阶段提交流程。
二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点:
由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂(数据不一致)等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点:
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit
、PreCommit
、DoCommit
三个阶段。
CanCommit阶段:
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
PreCommit阶段:
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
doCommit阶段:
该阶段进行真正的事务提交,也可以分为以下两种情况。
执行提交
中断事务:协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
因宕机导致的事务未知问题:
直接分析协调者和参与者都挂的情况:当第二阶段协调者和参与者宕机了,宕机了的这个参与者在宕机之前已经执行了操作。但是由于参与者宕机了,没有人知道参与者执行了什么操作。
单点故障与同步阻塞问题:
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交,而不会一直持有事务资源并处于阻塞状态。
但是这种机制也会导致数据一致性问题。因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
由此,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。
Paxos算法是分布式技术大师Lamport提出的,通过这个算法,让够参与分布式处理的每个参与者逐步达成一致意见。在分布式领域具有非常重要的地位。但是Paxos算法有两个比较明显的缺点:1.难以理解 2.工程实现更难。
聊聊分布式事务,再说说解决方案
再有人问你分布式事务,把这篇扔给他- 掘金
常用的分布式事务解决方案- 掘金
微服务架构设计模式
部分已寻不到原出处