1.1 什么是事务java
事务能够看作是一次大的活动,它由不一样的小活动组成,这些活动要么所有成功,要么所有失败。web
1.2 本地事务面试
在计算机系统中,更多的是经过关系型数据库来控制事务,这是利用数据库自己的事务特性来实现的,所以叫数据库事务,因为应用主要靠关系数据库来控制事务,而数据库一般和应用在同一个服务器,因此基于关系型数据库的事务又被称为本地事务。数据库
数据库事务的四大特性:ACID编程
A(Atomic):原子性,构成事务的全部操做,要么都执行完成,要么所有不执行,不可能出现部分红功部分失败的状况。后端
C(Consistency):一致性,在事务执行先后,数据库的一致性约束没有被破坏。好比:张三向李四转 100 元,转帐前和转帐后的数据是正确状态这叫一致性,若是出现张三转出 100 元,李四帐户没有增长 100 元这就出现了数 据错误,就没有达到一致性。服务器
I(Isolation):隔离性,数据库中的事务通常都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其余事务的运行过程的中间状态。经过配置事务隔离级别能够比避免脏读、重复读问题。微信
D(Durability):持久性,事务完成以后,该事务对数据的更改会持久到数据库,且不会被回滚。网络
数据库事务在实现时会将一次事务的全部操做所有归入到一个不可分割的执行单元,该执行单元的全部操做要么都成功,要么都失败,只要其中任一操做执行失败,都将致使整个事务的回滚。多线程
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:
分布式系统会把一个应用系统拆分为可独立部署的多个服务,所以须要服务与服务之间远程协做才能完成事务操做,这种分布式系统环境下由不一样的服务之间经过网络远程协做完成事务称之为分布式事务,例如用户注册送积分事务、建立订单减库存事务,银行转帐事务等都是分布式事务。
咱们知道本地事务依赖数据库自己提供的事务特性来实现,所以如下逻辑能够控制本地事务:
begin transaction; //1.本地数据库操做:张三减小金额 //2.本地数据库操做:李四增长金额commit transation;
可是在分布式环境下,会变成下边这样:
begin transaction; //1.本地数据库操做:张三减小金额 //2.远程调用:让李四增长金额commit transation;
能够设想,当远程调用让李四增长金额成功了,因为网络问题远程调用并无返回,此时本地事务提交失败就回滚了张三减小金额的操做,此时张三和李四的数据就不一致了。
所以在分布式架构的基础上,传统数据库事务就没法使用了,张三和李四的帐户不在一个数据库中甚至不在一个应用系统里,实现转帐事务须要经过远程调用,因为网络问题就会致使分布式事务问题。
跨JVM进程产生分布式事务
典型的场景就是微服务架构:微服务之间经过远程调用完成事务操做。好比:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减小库存。
跨数据库实例产生分布式事务
单体系统访问多个数据库实例当单体系统须要访问多个数据库(实例)时就会产生分布式事务。好比:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,须要分别删除用户信息及用户的订单信息,因为数据分布在不一样的数据实例,须要经过不一样的数据库连接去操做数据,此时产生分布式事务。
多服务访问同一个数据库实例
订单微服务和库存微服务即便访问同一个数据库也会产生分布式事务,缘由就是跨JVM进程,两个微服务持有了不一样的数据库连接进行数据库操做,此时产生分布式事务。
经过前面的学习,咱们了解到了分布式事务的基础概念。与本地事务不一样的是,分布式系统之因此叫分布式,是由于提供服务的各个节点分布在不一样机器上,相互之间经过网络交互。不能由于有一点网络问题就致使整个系统没法提供服务,网络因素成为了分布式事务的考量标准之一。所以,分布式事务须要更进一步的理论支持,接下来,咱们先来学习一下分布式事务的CAP理论。
CAP 是 Consistency、Availability、Partition tolerance 三个单词的缩写,分别表示一致性、可用性、分区容忍性。
下面为了方便对CAP理论的理解,咱们结合电商系统中的一些业务场景来理解CAP。
以下图,是商品信息管理的执行流程:
总体执行流程以下
商品服务请求主数据库写入商品信息(添加商品、修改商品、删除商品)
主数据库向商品服务响应写入成功
商品服务请求从数据库读取商品信息
一致性是指写操做后的读操做能够读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。
上图中,商品信息的读写要知足一致性就是要实现以下目标:
商品服务写入主数据库成功,则向从数据库查询新数据也成功。
商品服务写入主数据库失败,则向从数据库查询新数据也失败。
如何实现一致性?
写入主数据库后要将数据同步到从数据库。
写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以避免在新数据写入成功后,向从数据库查询到旧的数据。
分布式系统一致性的特色:
因为存在数据同步的过程,写操做的响应会有必定的延迟。
为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
若是请求数据同步失败的结点则会返回错误信息,必定不会返回旧数据。
可用性是指任何事务操做均可以获得响应结果,且不会出现响应超时或响应错误。
上图中,商品信息读取知足可用性就是要实现以下目标:
从数据库接收到数据查询的请求则当即可以响应数据查询结果。
从数据库不容许出现响应超时或响应错误。
如何实现可用性
写入主数据库后要将数据同步到从数据库。
因为要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
即时数据尚未同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,若是连旧数据也没有则能够按照约定返回一个默认信息,但不能返回错误或响应超时。
分布式系统可用性的特色:全部请求都有响应,且不会出现响应超时或响应错误
一般分布式系统的各各结点部署在不一样的子网,这就是网络分区,不可避免的会出现因为网络问题而致使结点之间通讯失败,此时仍可对外提供服务,这叫分区容忍性。
上图中,商品信息读写知足分区容忍性就是要实现以下目标:
主数据库向从数据库同步数据失败不影响读写操做。
其一个结点挂掉不影响另外一个结点对外提供服务。
如何实现分区容忍性?
尽可能使用异步取代同步操做,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。
添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。
分布式分区容忍性的特色:分区容忍性分是布式系统具有的基本能力
上边商品管理的例子是否同时具有 CAP 呢?
在全部分布式事务场景中不会同时具有 CAP 三个特性,由于在具有了P的前提下C和A是不能共存的
好比,下图知足了P即表示实现分区容忍:
本图分区容忍的含义是:
主数据库经过网络向从数据库同步数据,能够认为主从数据库部署在不一样的分区,经过网络进行交互。
当主数据库和从数据库之间的网络出现问题不影响主数据库和从数据库对外提供服务。
其中一个节点挂掉不影响另外一个节点对外提供服务。
若是要实现 C 则必须保证数据一致性,在数据同步的时候为防止向从数据库查询不一致的数据则须要将从数据库数据锁定,待同步完成后解锁,若是同步失败从数据库要返回错误信息或超时信息。
若是要实现 A 则必须保证数据可用性,无论任什么时候候均可以向从数据查询数据,则不会响应超时或返回错误信息。经过分析发如今知足P的前提下 C 和 A 存在矛盾性。
因此在生产中对分布式事务处理时要根据需求来肯定知足 CAP 的哪两个方面。
AP
放弃一致性,追求分区容忍性和可用性。这是不少分布式系统设计时的选择。
例如:上边的商品管理,彻底能够实现 AP,前提是只要用户能够接受所查询到的数据在必定时间内不是最新的便可。
一般实现 AP 都会保证最终一致性,后面将的 BASE 理论就是根据 AP 来扩展的,一些业务场景好比:订单退款,今日退款成功,明日帐户到帐,只要用户能够接受在必定的时间内到帐便可。
CP
放弃可用性,追求一致性和分区容错性,zookeeper 其实就是追求的强一致,又好比跨行转帐,一次转帐请求要等待双方银行系统都完成整个事务才算完成。
CA
放弃分区容忍性,即不进行分区,不考虑因为网络不通或结点挂掉的问题,则能够实现一致性和可用性。那么系统将不是一个标准的分布式系统,最经常使用的关系型数据就知足了 CA。
上边的商品管理,若是要实现 CA 则架构以下:
主数据库和从数据库中间不在进行数据同步,数据库能够响应每次的查询请求,经过事务隔离级别实现每一个查询请求均可以返回最新的数据。
CAP 是一个已经被证明的理论,一个分布式系统最多只能同时知足:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它能够做为咱们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,并且如今的集群规模愈来愈大,因此节点故障、网络故障是常态,并且要保证服务可用性达到 N 个 9(99.99..%),并要达到良好的响应性能来提升用户体验,所以通常都会作出以下选择:保证 P 和 A ,舍弃 C 强一致,保证最终一致性。
强一致性和最终一致性
CAP 理论告诉咱们一个分布式系统最多只能同时知足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP 即舍弃一致性,保证可用性和分区容忍性,可是在实际生产中不少场景都要实现一致性,好比前边咱们举的例子主数据库向从数据库同步数据,即便不要一致性,可是最终也要将数据同步成功来保证数据一致,这种一致性和 CAP 中的一致性不一样,CAP 中的一致性要求 在任什么时候间查询每一个结点数据都必须一致,它强调的是强一致性,可是最终一致性是容许能够在一段时间内每一个结点的数据不一致,可是通过一段时间每一个结点的数据必须一致,它强调的是最终数据的一致性。
Base 理论介绍
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,经过牺牲强一致性来得到可用性,当出现故障容许部分不可用但要保证核心功能可用,容许数据在一段时间内是不一致的,但最终达到一致状态。知足BASE理论的事务,咱们称之为“柔性事务”。
基本可用:分布式系统在出现故障时,容许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然能够正常浏览。
软状态:因为不要求强一致性,因此BASE容许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改成“成功”状态。
最终一致:最终一致是指通过一段时间后,全部节点数据都将会达到一致。如订单的"支付中"状态,最终会变 为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但须要必定时间的延迟、等待。
前面学习了分布式事务的基础理论,以理论为基础,针对不一样的分布式场景业界常见的解决方案有 2PC、3PC、TCC、可靠消息最终一致性、最大努力通知这几种。
2PC 即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2 是指两个阶段,P 是指准备阶段,C 是指提交阶段。
举例:张三和李四很久不见,老友约起聚餐,饭店老板要求先买单,才能出票。这时张三和李四分别抱怨近况不如意,囊中羞涩,都不肯意请客,这时只能AA。只有张三和李四都付款,老板才能出票安排就餐。但因为张三和李四都是铁公鸡,造成了尴尬的一幕:
准备阶段:老板要求张三付款,张三付款。老板要求李四付款,李四付款。
提交阶段:老板出票,两人拿票纷纷落座就餐。
例子中造成了一个事务,若张三或李四其中一人拒绝付款,或钱不够,店老板都不会给出票,而且会把已收款退回。
整个事务过程由事务管理器和参与者组成,店老板就是事务管理器,张3、李四就是事务参与者,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责本身本地事务的提交和回滚。
在计算机中部分关系数据库如 Oracle、MySQL 支持两阶段提交协议,以下图:
准备阶段(Prepare phase):事务管理器给每一个参与者发送 Prepare 消息,每一个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交。(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件)
提交阶段(commit phase):若是事务管理器收到了参与者的执行失败或者超时消息时,直接给每一个参与者发送回滚(Rollback)消息;不然,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操做,并释放事务处理过程当中使用的锁资源。注意:必须在最后阶段释放锁资源。
下图展现了2PC的两个阶段,分红功和失败两个状况说明:
成功状况
失败状况
2PC的传统方案是在数据库层面实现的,如 Oracle、MySQL 都支持 2PC 协议,为了统一标准减小行业内没必要要的对接成本,须要制定标准化的处理模型及接口标准,国际开放标准组织 Open Group 定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
为了让你们更明确 XA 方案的内容,下面以新用户注册送积分为例来讲明:
执行流程以下:
应用程序(AP)持有用户库和积分库两个数据源。
应用程序(AP)经过 TM 通知用户库 RM 新增用户,同时通知积分库RM为该用户新增积分,RM 此时并未提交事务,此时用户和积分资源锁定。
TM 收到执行回复,只要有一方失败则分别向其余 RM 发起回滚事务,回滚完毕,资源锁释放。
TM 收到执行回复,所有成功,此时向全部 RM 发起提交事务,提交完毕,资源锁释放。
DTP 模型定义以下角色:
AP(Application Program):即应用程序,能够理解为使用 DTP 分布式事务的程序。
RM(Resource Manager):即资源管理器,能够理解为事务的参与者,通常状况下是指一个数据库实例,经过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个 RM。全局事务是指分布式事务处理环境中,须要操做多个数据库共同完成一个工做,这个工做便是一个全局事务。
DTP 模型定义TM和RM之间通信的接口规范叫 XA,简单理解为数据库提供的 2PC 接口协议,基于数据库的 XA 协议来实现 2PC 又称为 XA 方案
以上三个角色之间的交互方式以下:
TM 向 AP 提供 应用程序编程接口,AP 经过 TM 提交及回滚事务。
TM 交易中间件经过 XA 接口来通知 RM 数据库事务的开始、结束以及提交、回滚等。
总结
整个 2PC 的事务流程涉及到三个角色 AP、RM、TM。AP 指的是使用 2PC 分布式事务的应用程序;RM 指的是资源管理器,它控制着分支事务;TM 指的是事务管理器,它控制着整个全局事务。
(1)在准备阶段 RM 执行实际的业务操做,但不提交事务,资源锁定
(2)在提交阶段 TM 会接受 RM 在准备阶段的执行回复,只要有任一个RM执行失败,TM 会通知全部 RM 执行回滚操做,不然,TM 将会通知全部 RM 提交该事务。提交阶段结束资源锁释放。
XA方案的问题
须要本地数据库支持XA协议。
资源锁须要等到两个阶段结束才释放,性能较差。
Seata 是由阿里中间件团队发起的开源项目 Fescar,后改名为 Seata,它是一个是开源的分布式事务框架。
传统 2PC 的问题在 Seata 中获得了解决,它经过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工做在应用层的中间件。主要优势是性能较好,且不长时间占用链接资源,它以高效而且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT 模式(即 2PC)及 TCC 模式的分布式事务解决方案。
Seata 的设计思想以下:
Seata 的设计目标其一是对业务无侵入,所以从业务无侵入的 2PC 方案着手,在传统 2PC的基础上演进,并解决 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一块儿成功提交,要么一块儿失败回滚。此外,一般分支事务自己就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:
与传统 2PC 的模型相似,Seata 定义了 3 个组件来协议分布式事务的处理过程:
Transaction Coordinator(TC):事务协调器,它是独立的中间件,须要独立部署运行,它维护全局事务的运行状态,接收 TM 指令发起全局事务的提交与回滚,负责与 RM 通讯协调各各分支事务的提交或回滚。
Transaction Manager(TM):事务管理器,TM 须要嵌入应用程序中工做,它负责开启一个全局事务,并最终向 TC 发起全局提交或全局回滚的指令。
Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。
还拿新用户注册送积分举例Seata的分布式事务过程:
具体的执行流程以下:
用户服务的 TM 向 TC 申请开启一个全局事务,全局事务建立成功并生成一个全局惟一的 XID。
用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其归入 XID 对应全局事务的管辖。
用户服务执行分支事务,向用户表插入一条记录。
逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的 RM 向 TC 注册分支事务,该分支事务执行增长积分的逻辑,并将其归入 XID 对应全局事务的管辖。
积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
用户服务分支事务执行完毕。
TM 向 TC 发起针对 XID 的全局提交或回滚决议。
TC 调度 XID 下管辖的所有分支事务完成提交或回滚请求。
Seata实现2PC与传统2PC的差异
架构层次方面:传统 2PC 方案的 RM 其实是在数据库层,RM 本质上就是数据库自身,经过 XA 协议实现,而 Seata 的 RM 是以 jar 包的形式做为中间件层部署在应用程序这一侧的。
两阶段提交方面:传统 2PC不管第二阶段的决议是 commit 仍是 rollback ,事务性资源的锁都要保持到 Phase2 完成才释放。而 Seata 的作法是在 Phase1 就将本地事务提交,这样就能够省去 Phase2 持锁的时间,总体提升效率。
本节讲解了传统 2PC(基于数据库 XA 协议)和 Seata 实现 2PC 的两种 2PC 方案,因为 Seata 的 0 侵入性而且解决了传统 2PC 长期锁资源的问题,推荐采用 Seata 实现 2PC。
TCC 是 Try、Confirm、Cancel 三个词语的缩写,TCC 要求每一个分支事务实现三个操做:预处理 Try、确认 Confirm、撤销 Cancel。Try 操做作业务检查及资源预留,Confirm 作业务确认操做,Cancel 实现一个与 Try 相反的操做即回滚操做。TM 首先发起全部的分支事务的 Try 操做,任何一个分支事务的Try操做执行失败,TM 将会发起全部分支事务的 Cancel 操做,若 Try 操做所有成功,TM 将会发起全部分支事务的 Confirm 操做,其中 Confirm/Cancel 操做若执行失败,TM 会进行重试。
分支事务成功状况:
分支事务失败状况:
TCC 分为三个阶段:
Try 阶段是作完业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操做,它和后续的 Confirm 一块儿才能真正构成一个完整的业务逻辑。
Confirm 阶段是作确认提交,Try 阶段全部分支事务执行成功后开始执行 Confirm。一般状况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 必定成功。若 Confirm 阶段真的出错了,需引入重试机制或人工处理。
Cancel 阶段是在业务执行错误须要回滚的状态下执行分支事务的业务取消,预留资源释放。一般状况下,采用 TCC 则认为 Cancel 阶段也是必定成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工处理。
TM 事务管理器
TM事务管理器能够实现为独立的服务,也可让全局事务发起方充当 TM 的角色,TM 独立出来是为了成为公 用组件,是为了考虑系统结构和软件复用。
TM 在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,因为 Confirm 和 Cancel 失败需进行重试,所以须要实现为幂等,幂等性是指同一个操做不管请求多少次,其结果都相同。
TCC须要注意三种异常处理分别是空回滚、幂等、悬挂
空回滚
在没有调用 TCC 资源 Try 方法的状况下,调用了二阶段的 Cancel 方法,Cancel 方法须要识别出这是一个空回滚,而后直接返回成功。
出现缘由是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候实际上是没有执行 Try 阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的 Cancel 方法,从而造成空回滚。
解决思路是关键就是要识别出这个空回滚。思路很简单就是须要知道一阶段是否执行,若是执行了,那就是正常回滚;若是没执行,那就是空回滚。前面已经说过 TM 在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条。再额外增长一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,若是该记录存在,则正常回滚;若是该记录不存在,则是空回滚。
幂等
经过前面介绍已经了解到,为了保证 TCC 二阶段提交重试机制不会引起数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。若是幂等控制没有作好,颇有可能致使数据不一致等严重问题。
解决思路在上述"分支事务记录"中增长执行状态,每次执行前都查询该状态。
悬挂
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
出现缘由是在 RPC 调用分支事务 Try 时,先注册分支事务,再执行 RPC 调用,若是此时 RPC 调用的网络发生拥堵,一般 RPC 调用是有超时时间的,RPC 超时之后,TM 就会通知 RM 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人可以处理了,对于这种状况,咱们就称为悬挂,即业务资源预留后无法继续处理。
解决思路是若是二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,"分支事务记录"表中是否已经有二阶段事务记录,若是有则不执行 Try。
举例,场景为 A 转帐 30 元给 B,A 和 B 帐户在不一样的服务。
方案
帐户 A
try: 检查余额是否够30元 扣减30元
confirm: 空
cancel: 增长30元
帐户 B
try: 增长30元
confirm: 空
cancel: 减小30元
方案说明
(1)帐户 A,这里的余额就是所谓的业务资源,按照前面提到的原则,在第一阶段须要检查并预留业务资源,所以,咱们在扣钱 TCC 资源的 Try 接口里先检查 A 帐户余额是否足够,若是足够则扣除 30 元。Confirm 接口表示正式提交,因为业务资源已经在 Try 接口里扣除掉了,那么在第二阶段的 Confirm 接口里能够什么都不用作。Cancel 接口的执行表示整个事务回滚,帐户A回滚则须要把 Try 接口里扣除掉的 30 元还给帐户。
(2)帐号B,在第一阶段 Try 接口里实现给帐户 B 加钱,Cancel 接口的执行表示整个事务回滚,帐户 B 回滚则须要把 Try 接口里加的 30 元再减去。
方案问题分析
若是帐户 A 的 Try 没有执行在 Cancel 则就多加了 30 元。
因为 Try、Cancel、Confirm 都是由单独的线程去调用,且会出现重复调用,因此都须要实现幂等。
帐号 B 在 Try 中增长 30 元,当 Try 执行完成后可能会其它线程给消费了。
若是帐户 B 的 Try 没有执行在 Cancel 则就多减了 30 元。
问题解决
帐户 A 的 Cancel 方法须要判断 Try 方法是否执行,正常执行 Try 后方可执行 Cancel。
Try、Cancel、Confirm方法实现幂等。
帐号 B 在 Try 方法中不容许更新帐户金额,在 Confirm 中更新帐户金额。
帐户 B 的 Cancel 方法须要判断 Try 方法是否执行,正常执行 Try 后方可执行 Cancel。
优化方案
帐户 A
try: try幂等校验 try悬挂处理 检查余额是否够30元 扣减30元
confirm: 空
cancel: cancel幂等校验 cancel空回滚处理 增长可用余额30元
帐户 B
try: 空confirm: confirm幂等校验 正式增长30元cancel: 空
若是拿 TCC 事务的处理流程与 2PC 两阶段提交作比较,2PC 一般都是在跨库的 DB 层面,而 TCC 则在应用层面的处理,须要经过业务逻辑来实现。这种分布式事务的实现方式的优点在于,可让应用本身定义数据操做的粒度,使得下降锁冲突、提升吞吐量成为可能。
而不足之处则在于对应用的侵入性很是强,业务逻辑的每一个分支都须要实现 Try、Confirm、Cancel 三个操做。此外,其实现难度也比较大,须要按照网络状态、系统故障等不一样的失败缘由实现不一样的回滚策略。
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)必定可以接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
此方案是利用消息中间件完成,以下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是经过网络通讯,因为网络通讯的不肯定性会致使分布式事务问题。
所以可靠消息最终一致性方案要解决如下几个问题:
本地事务与消息发送的原子性问题
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,不然就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。
下面这种操做,先发送消息,在操做数据库:
begin transaction; //1.发送MQ //2.数据库操做commit transation;
这种状况下没法保证数据库操做与发送消息的一致性,由于可能发送消息成功,数据库操做失败。那么第二种方案,先进行数据库操做,再发送消息:
begin transaction; //1.数据库操做 //2.发送MQcommit transation;
这种状况下貌似没有问题,若是发送 MQ 消息失败,就会抛出异常,致使数据库事务回滚。但若是是超时异常,数据库回滚,但 MQ 其实已经正常发送了,一样会致使不一致。
事务参与方接收消息的可靠性
事务参与方必须可以从消息队列接收到消息,若是接收消息失败能够重复接收消息。
消息重复消费的问题
因为网络2的存在,若某一个消费节点超时可是消费成功,此时消息中间件会重复投递此消息,就致使了消息的重复消费。
要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
上节讨论了可靠消息最终一致性事务方案须要解决的问题,本节讨论具体的解决方案。
本地消息表这个方案最初是 eBay 提出的,此方案的核心是经过本地事务保证数据业务操做和消息的一致性,而后经过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来讲明:下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增长积分。
交互流程以下:
用户注册
用户服务在本地事务新增用户和增长 "积分消息日志"。(用户表和消息表经过本地事务保证一致)
begin transaction //1.新增用户 //2.存储积分消息日志commit transation
这种状况下,本地数据库操做与存储积分消息日志处于同一个事务中,本地数据库操做与记录消息日志操做具有原子性。
定时任务扫描日志
如何保证将消息发送给消息队列呢?
通过第一步消息已经写到消息日志表中,能够启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,不然等待定时任务下一周期重试。
消费消息
如何保证消费者必定能消费到消息呢?
这里可使用 MQ 的 ack(即消息确认)机制,消费者监听 MQ,若是消费者接收到消息而且业务处理完成后向 MQ 发送 ack(即消息确认),此时说明消费者正常消费消息完成,MQ 将再也不向消费者推送消息,不然消费者会不断重试向消费者来发送消息。
积分服务接收到"增长积分"消息,开始增长积分,积分增长成功后向消息中间件回应 ack,不然消息中间件将重复投递此消息。
因为消息会重复投递,积分服务的"增长积分"功能须要实现幂等性。
可靠消息最终一致性就是保证消息从生产方通过消息中间件传递到消费方的一致性:
本地事务与消息发送的原子性问题。
事务参与方接收消息的可靠性。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操做变为基于消息执行的异步操做, 避免了分布式事务中的同步阻塞操做的影响,并实现了两个服务的解耦。
最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子:
交互流程:
帐户系统调用充值系统接口
充值系统完成支付处理向帐户发起充值结果通知,若通知失败,则充值系统按策略进行重复通知
帐户系统接收到充值结果通知修改充值状态
帐户系统未接收到通知会主动调用充值系统的接口查询充值结果
经过上边的例子咱们总结最大努力通知方案的目标:发起通知方经过必定的机制最大努力将业务处理结果通知到接收方。
具体包括:
有必定的消息重复通知机制。由于接收通知方可能没有接收到通知,此时要有必定的机制对消息重复通知
消息校对机制。若是尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来知足需求。
最大努力通知与可靠消息一致性有什么不一样?
解决方案思想不一样
可靠消息一致性,发起通知方须要保证将消息发出去,而且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,可是可能消息接收不到,此时须要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
二者的业务应用场景不一样
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
技术解决方向不一样
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出而且被接收到。最大努力通知没法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息没法被接收方接收时,由接收方主动查询消息(业务处理结果)
经过对最大努力通知的理解,采用 MQ 的 ack 机制就能够实现最大努力通知。
方案1:
本方案是利用 MQ 的 ack 机制由 MQ 向接收通知方发送通知,流程以下:
发起通知方将通知发给 MQ。使用普通消息机制将通知发给MQ。
注意:若是消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果。(后边会讲)
接收通知方监听 MQ。
接收通知方接收消息,业务处理完成回应 ack。
接收通知方若没有回应 ack 则 MQ 会重复通知。
MQ会按照间隔 1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔,直到达到通知要求的时间窗口上限。
接收通知方可经过消息校对接口来校对消息的一致性。
方案 2:
交互流程以下:
发起通知方将消息发给 MQ。
使用可靠消息一致方案中的事务消息保证本地事务和消息的原子性,最终将通知先发给 MQ。
通知程序监听 MQ,接收 MQ 的消息。
方案 1 中接收通知方直接监听 MQ,方案 2 中由通知程序监听 MQ。
通知程序若没有回应 ack 则 MQ 会重复通知。
通知程序经过互联网接口协议(如 http、webservice)调用接收通知方案接口,完成通知。
通知程序调用接收通知方案接口成功就表示通知成功,即消费 MQ 消息成功,MQ 将再也不向通知程序投递通知消息。
接收通知方可经过消息校对接口来校对消息的一致性。
方案1和方案2的不一样点:
方案 1 中接收通知方与 MQ 接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。
方案 2 中由通知程序与 MQ 接口,通知程序监听 MQ,收到 MQ 的消息后由通知程序经过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知。
最大努力通知方案是分布式事务中对一致性要求最低的一种,适用于一些最终一致性时间敏感度低的业务;最大努力通知方案须要实现以下功能:
消息重复通知机制
消息校对机制
分布式事务对比分析
2PC 最大的诟病是一个阻塞协议。RM 在执行分支事务后须要等待 TM 的决定,此时服务会阻塞并锁定资源。因为其阻塞机制和最差时间复杂度高,所以,这种设计不能适应随着事务涉及的服务数量增长而扩展的须要,很难用于并发较高以及子事务生命周期较长(long-running transactions) 的分布式服务中。
若是拿TCC事务的处理流程与2PC两阶段提交作比较,2PC 一般都是在跨库的 DB 层面,而 TCC 则在应用层面的处理,须要经过业务逻辑来实现。这种分布式事务的实现方式的优点在于,可让应用本身定义数据操做的粒度,使得下降锁冲突、提升吞吐量成为可能。而不足之处则在于对应用的侵入性很是强,业务逻辑的每一个分支都须要实现 Try、Confirm、Cancel 三个操做。此外,其实现难度也比较大,须要按照网络状态、系统故障等不一样的失败缘由实 现不一样的回滚策略。典型的使用场景:满减,登陆送优惠券等。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操做变为基于消息执行的异步操做, 避免了分布式事务中的同步阻塞操做的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登陆送优惠券等。
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;容许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,不管发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行状况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等。
2PC | TCC | 可靠消息 | 最大努力通知 | |
---|---|---|---|---|
一致性 | 强一致性 | 最终一致性 | 最终一致性 | 最终一致性 |
吞吐量 | 低 | 中 | 高 | 高 |
实现复杂度 | 易 | 难 | 中 | 易 |
总结
在条件容许的状况下,咱们尽量选择本地事务单数据源,由于它减小了网络交互带来的性能损耗,且避免了数据弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从总体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度过小?分布式事务一直是业界难题,由于网络的不肯定性,并且咱们习惯于拿分布式事务与单机事务 ACID 作对比。
不管是数据库层的 XA、仍是应用层 TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们不过是各自在性能、一致性、可用性等方面作取舍,寻求某些场景偏好下的权衡。
关注做者微信公众号 —《JAVA烂猪皮》
了解更多java后端架构知识以及最新面试宝典