精华内容
下载资源
问答
  • 对外事务专员岗位说明书拥有着完美的一致性、社会性、明确性和层次性的特性,喜欢对外事务专员岗位说明书...该文档为对外事务专员岗位说明书,是一份很不错的参考资料,具有较高参考价值,感兴趣的可以下载看看
  • 分布式事务什么?如何实现?

    千次阅读 热门讨论 2021-04-12 00:03:16
    一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。 从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 实现方式: ...

    写在前面

    本文隶属于专栏《100个问题搞定大数据理论体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

    本专栏目录结构和文献引用请见100个问题搞定大数据理论体系

    解答

    一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
    从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
    
    实现方式:
    1.2PC
    2.本地消息表
    3.3PC
    4.TCC
    5.消息事务+最终一致性
    

    在这里插入图片描述

    补充

    2PC

    两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终決定这些参与者是否要真正执行事务。

    运行过程

    第一阶段——准备

    协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成种投票,需要参与者都同意才能执行。

    第二阶段——提交

    如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务; 否则,协调者发送通知让参与者回滚事务需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

    2PC

    存在的问题

    1. 同步阻塞所有事务参与者在等待其它参与者响应的时候都处于同步阻塞等待状态,无法进行其它操作。
    2. 单点问题协调者在2PC中起到非常大的作用,发生故障将会造成很大影响。特别是在提交阶段发生故障,所有参与者会一直同步阻塞等待,无法完成其它操作。
    3. 数据不一致在提交阶段,如果协调者只发送了部分Commit消息,此时网络发生异常,那么只有部分参与者接收到Commit消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
    4. 太过保守任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

    本地消息表

    本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。

    1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
    2. 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
    3. 在分布式事务操作的另ー方从消息队列中读取一个消息,并执行消息中的操作。

    3PC

    3PC其实在2PC的基础上增加了CanCommit阶段,是2PC的变种,并引入了超时机制。
    一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效的解决了协调者单点故障的问题。
    但是,性能和数据一致性问题没有根本解决。

    3PC分为三个阶段:CanCommit、PreCommit、DoCommit

    CanCommit阶段

    它跟2PC的 准备阶段很像,协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

    事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应
    响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则返回No

    PreCommit阶段

    协调者根据参与者的响应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能:

    1. 假如协调者从所有的参与者获得的反馈都是Yes,那么就会执行事务的与执行。
      发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
      事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
      响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
    2. 假如有任何一个参与者向协调者发送了No响应,或者等待超时,或者协调者都没有接到参与者的响应,那么就执行事务的中断。
      发送中断请求:协调者向所有参与者发送abort请求。
      中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

    DoCommit阶段

    该阶段进行真正的事务提交,也可以分为以下两种情况:

    执行提交

    1. 发送提交请求:协调接收到参与者发送的ACK响应,那么将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    2. 事务提交:参与者接收到doCommit请求之后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源。
    3. 响应反馈:事务提交完之后,向协调者发送ACK响应。
    4. 完成事务:协调者接收到所有参与者的ACK响应之后,完成事务。

    中断事务

    协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

    1. 发送中断请求:协调者向所有参与者发送abort请求
    2. 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    3. 反馈结果:参与者完成事务回滚之后,像协调者发送ACK消息。
    4. 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

    3PC

    存在的问题

    相对于2PC而言,3PC对于协调者和参与者都设置了超时时间,而2PC只有协调者才拥有超时时间机制。
    这个优化解决了,参与者在长时间无法与协调者节点通讯的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。
    而这种机制也侧面降低了整个事务的阻塞时间和范围。但是仍然没有解决数据一致性问题,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

    补偿事务(TCC)

    TCC(Try-Confirm-Cancel)又称补偿事务。它实际上与2PC、3PC一样,都是分布式事务的一种实现方案而已。它分为三个操作:

    Try阶段:主要是对业务系统做检测及资源预留。
    Confirm阶段:确认执行业务操作。
    Cancel阶段:取消执行业务操作。
    TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在DB层面,而TCC本质上就是应用层面的2PC,需要通过业务逻辑来实现。它的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提交吞吐量。

    不过对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。

    TCC

    消息事务+最终一致性

    所谓的消息事务就是基于消息中间件的两阶段提交,本质上是中间件的一种特殊利用,他是将本地事务和发消息放在一个分布式事务里,保证要么本地操作成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:

    消息事务

    实现步骤

    1. 服务A向消息中间件发送一条预备消息。
    2. 消息中间件保存预备消息并返回成功。
    3. 服务A执行本地事务。
    4. 服务A发送提交消息给消息中间件,服务B接收到消息之后执行本地事务。

    基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(服务A的本地操作+发消息)+服务B的本地操作,其中服务B的操作由消息驱动,只要消息事务成功,那么服务A一定成功,消息也一定发出来了,这时候服务B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到服务B操作成功,这样就变相地实现了A与B的分布式事务。

    异常情况分析

    1. 步骤一出错:则整个事务失败,不会执行服务A的本地操作。
    2. 步骤二出错:则整个事务失败,不会执行服务A的本地操作。
    3. 步骤三出错:需要做回滚预备消息,由服务A实现一个消息中间件的回调接口,消息中间件会不断执行回调接口,检查服务A事务执行是否执行成功,如果失败则回滚预备消息。
    4. 步骤四出错:这个时候服务A的本地事务是成功的,但是消息中间件不需要回滚,其实通过回调接口,消息中间件能够检查到服务A执行成功了,这个时候其实不需要服务发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。
    展开全文
  • 目录概念发展历程核心组件三种事务模式AT模式前提一个分布式事务在Seata中的执行流程MySQL XA方案MySQL XA中拥有两种角色:TCC模式Saga模式 概念 Seata(原名Fescar) 是 阿里巴巴 开源的 分布式事务中间件,以 高效...

    最后更新于:2020-04-08 17:37

    概念

    Seata(原名Fescar) 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题。

    发展历程

    2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。

    2016 年,TXC 经过产品化改造,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品,在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。

    2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。

    Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。

    为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为 Seata,意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。

    核心组件

    Seata官方针对TXC模型制作的示意图:
    在这里插入图片描述
    TXC的实现通过三个组件来完成。也就是上图的三个深黄色部分,其作用如下:

    TC(Transaction Coordinator): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚(server端)。这个组件需要独立部署维护,目前0.6.1版本已经支持集群部署。

    TM(Transaction Manager): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议(client端)。

    RM(Resource Manager):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚(client端)。

    TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。

    从模型图中可以看出,这三者的核心是TC,下图是Seata-Server整体的模块图:
    在这里插入图片描述
    1)Coordinator Core:最下面的模块是事务协调器核心代码,主要用来处理事务协调的逻辑,如是否 Commit、Rollback 等协调活动。

    2)Store:存储模块,用来将我们的数据持久化,防止重启或者宕机数据丢失。

    3)Discover:服务注册/发现模块,用于将 Server 地址暴露给 Client。

    4)Config:用来存储和查找服务端的配置。

    5)Lock:锁模块,用于给 Seata 提供全局锁的功能。

    6)Rpc:用于和其他端通信。

    7)HA-Cluster:高可用集群,目前还没开源。为 Seata 提供可靠的高可用功能。

    这里暂不做详细讲解,可参考:http://seata.io/zh-cn/blog/seata-analysis-java-server.html

    三种事务模式

    AT模式

    今年 1 月份,Seata 开源了 AT 模式。AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

    前提

    1.基于支持本地 ACID 事务的关系型数据库。
    2.Java 应用,通过 JDBC 访问数据库。

    一个分布式事务在Seata中的执行流程

    在这里插入图片描述
    执行步骤:
    1.TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID(由 ip:port:sequence 组成)。XID 在微服务调用链路的上下文中传播。
    2.RM 向 TC 注册分支事务,接着执行这个分支事务并提交,最后将执行结果汇报给TC。
    3.TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
    4.TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

    Seata 的事务提交方式与 XA 协议的两段式提交有什么不同呢?

    XA协议

    在这里,先说一下XA协议,以下是百度百科的介绍:
    XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。XA协议包括两套函数,以xa_开头的及以ax_开头的。
    以下的函数是事务管理器可以对资源管理器进行的操作:
    1)xa_open,xa_close:建立和关闭与资源管理器的连接。
    2)xa_start,xa_end:开始和结束一个本地事务。
    3)xa_prepare,xa_commit,xa_rollback:预提交、提交和回滚一个本地事务。
    4)xa_recover:回滚一个已进行预提交的事务。
    5)ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
    6)ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册。

    以MySQL XA方案为例
    RM(Resource Manager):资源管理器,用于直接执行本地事务的提交和回滚。在分布式集群中,一台MySQL服务器就是一个RM。
    TM(Transaction Manager):事务管理器,它是分布式事务的核心管理者。事务管理器与每个RM进行通信,协调并完成分布式事务的处理。发起一个分布式事务的MySQL客户端就是一个TM。

    XA的两阶段提交分为Prepare阶段和Commit阶段,过程如下:

    阶段一为准备阶段(prepare)。即所有的RM锁住需要的资源,在本地执行这个事务(执行sql,写redo/undo log等),但不提交,然后向Transaction Manager报告已准备就绪。
    阶段二为提交阶段(commit)。当Transaction Manager确认所有参与者都ready后,向所有参与者发送commit命令。
    在这里插入图片描述
    XA 协议它依赖的是数据库层面来保障事务的一致性,也即是说 XA 的各个分支事务是在数据库层面上驱动的,由于 XA 的各个分支事务需要有 XA 的驱动程序,一方面会导致数据库与 XA 驱动耦合,另一方面它会导致各个分支的事务资源锁定周期长(直到两阶段提交完成才释放资源)。

    基于这些问题,Seata另辟蹊径,在应用层做手脚,Seata的RM模块内部做了对数据库操作的代理层,如下:
    在这里插入图片描述

    Seata分支事务是如何提交和回滚的呢?

    第一阶段
    分支事务利用 RM 模块中对 JDBC 数据源代理,加入了若干流程,对业务 SQL 进行解释,把业务数据在更新前后的数据镜像组织成回滚日志,并生成 undo log 日志,对全局事务锁的检查以及分支事务的注册等,利用本地事务 ACID 特性,将业务 SQL 和 undo log 写入同一个事物中一同提交到数据库中,保证业务 SQL 必定存在相应的回滚日志,最后对分支事务状态向 TC 进行上报。
    在这里插入图片描述
    第二阶段

    • TM决议全局提交:

    当 TM 决议提交时,就不需要同步协调处理了,TC 会异步调度各个 RM 分支事务删除对应的 undo log 日志即可,这个步骤非常快速地可以完成。这个机制对于性能提升非常关键,我们知道正常的业务运行过程中,事务执行的成功率是非常高的,因此可以直接在本地事务中提交,这步对于提升性能非常显著。
    在这里插入图片描述

    • TM决议全局回滚:

    当 TM 决议回滚时,RM 收到 TC 发送的回滚请求,RM 通过 XID 找到对应的 undo log 回滚日志,然后利用本地事务 ACID 特性,执行回滚日志完成回滚操作并删除 undo log 日志,最后向 TC 进行回滚结果上报。

    在这里插入图片描述
    业务对以上所有的流程都无感知,业务完全不关心全局事务的具体提交和回滚,而且最重要的一点是 Seata 将两段式提交的同步协调分解到各个分支事务中了,分支事务与普通的本地事务无任何差异,这意味着我们使用 Seata 后,分布式事务就像使用本地事务一样,完全将数据库层的事务协调机制交给了中间件层 Seata 去做了,这样虽然事务协调搬到应用层了,但是依然可以做到对业务的零侵入,从而剥离了分布式事务方案对数据库在协议支持上的要求,且 Seata 在分支事务完成之后直接释放资源,极大减少了分支事务对资源的锁定时间,完美避免了 XA 协议需要同步协调导致资源锁定时间过长的问题。

    为了加深理解,我们用一个示例来更具体的说明AT模式下分支事务的工作过程:
    业务表:product

    FieldTypeKey
    idbigint(20)PRI
    namevarchar(100)
    sincevarchar(100)

    分支事务的业务逻辑:

    update product set name = 'GTS' where name = 'TXC';
    

    一阶段

    过程:

    1.解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
    2.查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

    select id, name, since from product where name = 'TXC';
    

    得到前镜像:

    idnamesince
    1TXC2014

    3.执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
    4.查询后镜像:根据前镜像的结果,通过 主键 定位数据。

    select id, name, since from product where id = 1`;
    

    得到后镜像:

    idnamesince
    1GTS2014

    5.插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

    {
    	"branchId": 641789253,
    	"undoItems": [{
    		"afterImage": {
    			"rows": [{
    				"fields": [{
    					"name": "id",
    					"type": 4,
    					"value": 1
    				}, {
    					"name": "name",
    					"type": 12,
    					"value": "GTS"
    				}, {
    					"name": "since",
    					"type": 12,
    					"value": "2014"
    				}]
    			}],
    			"tableName": "product"
    		},
    		"beforeImage": {
    			"rows": [{
    				"fields": [{
    					"name": "id",
    					"type": 4,
    					"value": 1
    				}, {
    					"name": "name",
    					"type": 12,
    					"value": "TXC"
    				}, {
    					"name": "since",
    					"type": 12,
    					"value": "2014"
    				}]
    			}],
    			"tableName": "product"
    		},
    		"sqlType": "UPDATE"
    	}],
    	"xid": "xid:xxx"
    }
    

    6.提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
    7.本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
    8.将本地事务提交的结果上报给 TC。

    二阶段-回滚
    1.收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
    2.通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
    3.数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明后续介绍。
    4.根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:

    update product set name = 'TXC' where id = 1;
    

    5.提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

    二阶段-提交
    1.收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
    2.异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

    附:回滚日志表
    UNDO_LOG Table:不同数据库在类型上会略有差别。

    以 MySQL 为例:

    FieldType
    branch_idbigint PK
    xidvarchar(100)
    contextvarchar(128)
    rollback_infolongblob
    log_statustinyint
    log_createddatetime
    log_modifieddatetime
    CREATE TABLE `undo_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `branch_id` bigint(20) NOT NULL,
      `xid` varchar(100) NOT NULL,
      `context` varchar(128) NOT NULL,
      `rollback_info` longblob NOT NULL,
      `log_status` int(11) NOT NULL,
      `log_created` datetime NOT NULL,
      `log_modified` datetime NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    

    到这,我们已经很清楚,Seata在第一阶段就直接提交了分支的事务,这势必会造成隔离性的问题,所以Seata在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,AT 模式的默认全局隔离级别是 读未提交(Read Uncommitted),当然它也支持读已提交的隔离级别,接下来我们看下它是如何实现的:

    首先来看下Seata是如何实现写隔离的

    写隔离

    • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
    • 拿不到 全局锁 ,不能提交本地事务。
    • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
      以一个示例来说明:

    两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

    tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁

    在这里插入图片描述

    tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

    在这里插入图片描述

    如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

    此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试(使用事件监听模式,轮询访问数据库undo_log查看是否有未回滚的数据记录,如果有则立即执行回滚操作),直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

    因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

    接着看看如果必须要求全局 读已提交,Seata是如何实现读隔离的:

    读隔离

    如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理
    在这里插入图片描述
    SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

    出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

    TCC模式

    2019 年 3 月份,Seata 开源了 TCC 模式,该模式由蚂蚁金服贡献。TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
    在这里插入图片描述

    TCC模式与AT模式的区别

    从单系统到微服务转变,其实是一个资源横向扩展的过程,资源的横向扩展是指当单台机器达到资源性能瓶颈,无法满足业务增长需求时,就需要横向扩展资源,形成集群。通过横向扩展资源,提升非热点数据的并发性能,这对于大体量的互联网产品来说,是至关重要的。服务的拆分,也可以认为是资源的横向扩展,只不过方向不同而已。

    资源横向扩展可能沿着两个方向发展,包括业务拆分和数据分片

    • 业务拆分。根据功能对数据进行分组,并将不同的微服务分布在多个不同的数据库上,这实际上就是 SOA 架构下的服务化。业务拆分就是把业务逻辑从一个单系统拆分到多个微服务中。
    • 数据分片。在微服务内部将数据拆分到多个数据库上,为横向扩展增加一个新的维度。数据分片就是把一个微服务下的单个 DB 拆分成多个 DB,具备一个 Sharding 的功能。通过这样的拆解,相当于一种资源的横向扩展,从而使得整个架构可以承载更高的吞吐。

    横向扩展的两种方法可以同时进行运用:交易、支付与账务三个不同微服务可以存储在不同的数据库中。另外,每个微服务内根据其业务量可以再拆分到多个数据库中,各微服务可以相互独立地进行扩展。

    Seata 关注的就是微服务架构下的数据一致性问题,是一整套的分布式事务解决方案。而AT 模式主要从数据分片的角度,关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题。TCC 模式则主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题,保证读资源访问的事务属性。

    TCC模式框架模型

    在AT 模式下,就是把每个数据库当做一个 Resource,在本地事务提交时会去注册一个分支事务。那么对应到 TCC 模式里,也是一样的,Seata 框架把每组 TCC 接口当做一个 Resource,称为 TCC Resource。这套 TCC 接口可以是 RPC,也以是服务内 JVM 调用。在业务启动时,Seata 框架会自动扫描识别到 TCC 接口的调用方和发布方。如果是 RPC 的话,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。

    扫描到 TCC 接口的调用方和发布方之后。如果是发布方,会在业务启动时向 TC 注册 TCC Resource,与 DataSource Resource 一样,每个资源也会带有一个资源 ID。

    如果是调用方,Seata 框架会给调用方加上切面,与 AT 模式一样,在运行时,该切面会拦截所有对 TCC 接口的调用。每调用一次 Try 接口,切面会先向 TC 注册一个分支事务,然后才去执行原来的 RPC 调用。当请求链路调用完成后,TC 通过分支事务的资源 ID 回调到正确的参与者去执行对应 TCC 资源的 Confirm 或 Cancel 方法。

    TCC 业务模式与并发控制:

    1.TCC 设计原则

    从 TCC 模型的框架可以发现,TCC 模型的核心在于 TCC 接口的设计。用户在接入 TCC 时,大部分工作都集中在如何实现 TCC 服务上。设计一套 TCC 接口最重要的是什么?主要有两点:第一点,需要将操作分成两阶段完成。
    TCC 模型认为对于业务系统中一个特定的业务逻辑 ,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑:

    • 初步操作 Try:完成所有业务检查,预留必须的业务资源。
    • 确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。
    • 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
      第二点,就是要根据自身的业务模型控制并发,这个对应 ACID 中的隔离性。

    2.账务系统模型设计

    下面我们以金融核心链路里的账务服务来分析一下。首先一个最简化的账务模型就是图中所列,每个用户或商户有一个账户及其可用余额。然后,分析下账务服务的所有业务逻辑操作,无论是交易、充值、转账、退款等,都可以认为是对账户的加钱与扣钱。
    在这里插入图片描述
    因此,我们可以把账务系统拆分成两套 TCC 接口,即两个 TCC Resource,一个是加钱 TCC 接口,一个是扣钱 TCC 接口。

    那这两套接口分别需要做什么事情呢?如何将其分成两个阶段完成?下面将会举例说明 TCC 业务模式的设计过程,并逐渐优化。

    我们先来看扣钱的 TCC 资源怎么实现。场景为 A 转账 30 元给 B。账户 A 的余额中有 100 元,需要扣除其中 30 元。这里的余额就是所谓的业务资源,按照前面提到的原则,在第一阶段需要检查并预留业务资源,因此,我们在扣钱 TCC 资源的 Try 接口里先检查 A 账户余额是否足够,然后预留余额里的业务资源,即扣除 30 元。

    在这里插入图片描述
    在 Confirm 接口,由于业务资源已经在 Try 接口里扣除掉了,那么在第二阶段的 Confirm 接口里,可以什么都不用做。而在 Cancel 接口里,则需要把 Try 接口里扣除掉的 30 元还给账户。这是一个比较简单的扣钱 TCC 资源的实现,后面会继续优化它。

    而在加钱的 TCC 资源里。在第一阶段 Try 接口里不能直接给账户加钱,如果这个时候给账户增加了可用余额,那么在一阶段执行完后,账户里的钱就可以被使用了。但是一阶段执行完以后,有可能是要回滚的。因此,真正加钱的动作需要放在 Confirm 接口里。对于加钱这个动作,第一阶段 Try 接口里不需要预留任何资源,可以设计为空操作。那相应的,Cancel 接口没有资源需要释放,也是一个空操作。只有真正需要提交时,再在 Confirm 接口里给账户增加可用余额。

    这就是一个最简单的扣钱和加钱的 TCC 资源的设计。在扣钱 TCC 资源里,Try 接口预留资源扣除余额,Confirm 接口空操作,Cancel 接口释放资源,增加余额。在加钱 TCC 资源里,Try 接口无需预留资源,空操作;Confirm 接口直接增加余额;Cancel 接口无需释放资源,空操作。

    3.账务系统模型并发控制

    之前提到,设计一套 TCC 接口需要有两点,一点是需要拆分业务逻辑成两阶段完成。这个我们已经介绍了。另外一点是要根据自身的业务模型控制并发。

    Seata 框架本身仅提供两阶段原子提交协议,保证分布式事务原子性。事务的隔离需要交给业务逻辑来实现。隔离的本质就是控制并发,防止并发事务操作相同资源而引起的结果错乱。

    举个例子,比如金融行业里管理用户资金,当用户发起交易时,一般会先检查用户资金,如果资金充足,则扣除相应交易金额,增加卖家资金,完成交易。如果没有事务隔离,用户同时发起两笔交易,两笔交易的检查都认为资金充足,实际上却只够支付一笔交易,结果两笔交易都支付成功,导致资损。

    可以发现,并发控制是业务逻辑执行正确的保证,但是像两阶段锁这样的并发访问控制技术要求一直持有数据库资源锁直到整个事务执行结束,特别是在分布式事务架构下,要求持有锁到分布式事务第二阶段执行结束,也就是说,分布式事务会加长资源锁的持有时间,导致并发性能进一步下降。

    因此,TCC 模型的隔离性思想就是通过业务的改造,在第一阶段结束之后,从底层数据库资源层面的加锁过渡为上层业务层面的加锁,从而释放底层数据库锁资源,放宽分布式事务锁协议,将锁的粒度降到最低,以最大限度提高业务并发性能。

    还是以上面的例子举例,“账户 A 上有 100 元,事务 T1 要扣除其中的 30 元,事务 T2 也要扣除 30 元,出现并发”。在第一阶段 Try 操作中,需要先利用数据库资源层面的加锁,检查账户可用余额,如果余额充足,则预留业务资源,扣除本次交易金额,一阶段结束后,虽然数据库层面资源锁被释放了,但这笔资金被业务隔离,不允许除本事务之外的其它并发事务动用。

    在这里插入图片描述
    并发的事务 T2 在事务 T1 一阶段接口结束释放了数据库层面的资源锁以后,就可以继续操作,跟事务 T1 一样,加锁,检查余额,扣除交易金额。

    事务 T1 和 T2 分别扣除的那一部分资金,相互之间无干扰。这样在分布式事务的二阶段,无论 T1 是提交还是回滚,都不会对 T2 产生影响,这样 T1 和 T2 可以在同一个账户上并发执行。

    大家可以感受下,一阶段结束以后,实际上采用业务加锁的方式,隔离账户资金,在第一阶段结束后直接释放底层资源锁,该用户和卖家的其他交易都可以立刻并发执行,而不用等到整个分布式事务结束,可以获得更高的并发交易能力。

    这里稍微有点抽象,下面我们将会针对业务模型进行优化,大家可以更直观的感受业务加锁的思想。

    4.账务系统模型优化

    前面的模型大家肯定会想,为啥一阶段就把钱扣除了?是的。之前只是为了简单说明 TCC 模型的设计思想。在实际中,为了更好的用户体验,在第一阶段,一般不会直接把账户的余额扣除,而是冻结,这样给用户展示的时候,就可以很清晰的知道,哪些是可用余额,哪些是冻结金额。

    那业务模型变成什么样了呢?如图所示,需要在业务模型中增加冻结金额字段,用来表示账户有多少金额处以冻结状态。

    在这里插入图片描述
    既然业务模型发生了变化,那扣钱和加钱的 TCC 接口也应该相应的调整。还是以前面的例子来说明。

    在扣钱的 TCC 资源里。Try 接口不再是直接扣除账户的可用余额,而是真正的预留资源,冻结部分可用余额,即减少可用余额,增加冻结金额。Confirm 接口也不再是空操作,而是使用 Try 接口预留的业务资源,即将该部分冻结金额扣除;最后在 Cancel 接口里,就是释放预留资源,把 Try 接口的冻结金额扣除,增加账户可用余额。加钱的 TCC 资源由于不涉及冻结金额的使用,所以无需更改。

    通过这样的优化,可以更直观的感受到 TCC 接口的预留资源、使用资源、释放资源的过程。

    那并发控制又变成什么样了呢?跟前面大部分类似,在事务 T1 的第一阶段 Try 操作中,先锁定账户,检查账户可用余额,如果余额充足,则预留业务资源,减少可用余额,增加冻结金额。并发的事务 T2 类似,加锁,检查余额,减少可用余额金额,增加冻结金额。

    这里可以发现,事务 T1 和 T2 在一阶段执行完成后,都释放了数据库层面的资源锁,但是在各自二阶段的时候,相互之间并无干扰,各自使用本事务内第一阶段 Try 接口内冻结金额即可。这里大家就可以直观感受到,在每个事务的第一阶段,先通过数据库层面的资源锁,预留业务资源,即冻结金额。虽然在一阶段结束以后,数据库层面的资源锁被释放了,但是第二阶段的执行并不会被干扰,这是因为数据库层面资源锁释放以后通过业务隔离的方式为这部分资源加锁,不允许除本事务之外的其它并发事务动用,从而保证该事务的第二阶段能够正确顺利的执行。

    通过这两个例子,为大家讲解了怎么去设计一套完备的 TCC 接口。最主要的有两点,一点是将业务逻辑拆分成两个阶段完成,即 Try、Confirm、Cancel 接口。其中 Try 接口检查资源、预留资源、Confirm 使用资源、Cancel 接口释放预留资源。另外一点就是并发控制,采用数据库锁与业务加锁的方式结合。由于业务加锁的特性不影响性能,因此,尽可能降低数据库锁粒度,过渡为业务加锁,从而提高业务并发能力。

    相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。

    TCC 异常控制

    1. 空回滚:

    **什么是空回滚?**空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

    **什么样的情形会造成空回滚呢?**可以看图中的第 2 步,前面讲过,注册分支事务是在调用 RPC 时,Seata 框架的切面会拦截到该次调用请求,先向 TC 注册一个分支事务,然后才去执行 RPC 调用逻辑。如果 RPC 调用逻辑有问题,比如调用方机器宕机、网络异常,都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,需要推进到终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。

    在这里插入图片描述

    **那会不会有空提交呢?**理论上来说不会的,如果调用方宕机,那分布式事务默认是回滚的。如果是网络异常,那 RPC 调用失败,发起方应该通知 TC 回滚分布式事务,这里可以看出为什么是理论上的,就是说发起方可以在 RPC 调用失败的情况下依然通知 TC 提交,这时就会发生空提交,这种情况要么是编码问题,要么开发同学明确知道需要这样做。

    **那怎么解决空回滚呢?**前面提到,Cancel 要识别出空回滚,直接返回成功。那关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。因此,需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

    2.幂等控制:

    **什么是幂等?**幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。

    **什么样的情形会造成重复提交或回滚?**从图中可以看到,提交或回滚是一次 TC 到参与者的网络调用。因此,网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。

    在这里插入图片描述
    怎么解决重复执行的幂等问题呢?一个简单的思路就是记录每个分支事务的执行状态。在执行前状态,如果已执行,那就不再执行;否则,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记录每个分支事务的执行状态。

    3. 防悬挂控制:

    **什么是悬挂?**悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经结束,也就是说,当出现这种情况时,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

    **什么样的情况会造成悬挂呢?**按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,发起方就会通知 TC 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者,真正执行,从而造成悬挂。

    **怎么实现才能做到防悬挂呢?**根据悬挂出现的条件先来分析下,悬挂是指二阶段 Cancel 执行完后,一阶段才执行。也就是说,为了避免悬挂,如果二阶段执行完成,那一阶段就不能再继续执行。因此,当一阶段执行时,需要先检查二阶段是否已经执行完成,如果已经执行,则一阶段不再执行;否则可以正常执行。那怎么检查二阶段是否已经执行呢?大家是否想到了刚才解决空回滚和幂等时用到的事务控制表,可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

    在这里插入图片描述
    如图所示,该状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。二阶段 Confirm 和 Cancel 方法执行后修改为已提交或已回滚状态。当重复调用二阶段接口时,先获取该事务控制表对应记录,检查状态,如果已执行,则直接返回成功;否则正常执行。

    4.异常控制实现

    在分析完空回滚、幂等、悬挂等异常 Case 的成因以及解决方案以后,下面我们就综合起来考虑,一个 TCC 接口如何完整的解决这三个问题。

    首先是 Try 方法。结合前面讲到空回滚和悬挂异常,Try 方法主要需要考虑两个问题,一个是 Try 方法需要能够告诉二阶段接口,已经预留业务资源成功。第二个是需要检查第二阶段是否已经执行完成,如果已完成,则不再执行。先插入事务控制表记录,如果插入成功,说明第二阶段还没有执行,可以继续执行第一阶段。如果插入失败,则说明第二阶段已经执行或正在执行,则抛出异常,终止即可。

    接下来是 Confirm 方法。因为 Confirm 方法不允许空回滚,也就是说,Confirm 方法一定要在 Try 方法之后执行。因此,Confirm 方法只需要关注重复提交的问题。可以先锁定事务记录,如果事务记录为空,则说明是一个空提交,不允许,终止执行。如果事务记录不为空,则继续检查状态是否为初始化,如果是,则说明一阶段正确执行,那二阶段正常执行即可。如果状态是已提交,则认为是重复提交,直接返回成功即可;如果状态是已回滚,也是一个异常,一个已回滚的事务,不能重新提交,需要能够拦截到这种异常情况,并报警。

    最后是 Cancel 方法。因为 Cancel 方法允许空回滚,并且要在先执行的情况下,让 Try 方法感知到 Cancel 已经执行,所以和 Confirm 方法略有不同。首先依然是锁定事务记录。如果事务记录为空,则认为 Try 方法还没执行,即是空回滚。空回滚的情况下,应该先插入一条事务记录,确保后续的 Try 方法不会再执行。如果插入成功,则说明 Try 方法还没有执行,空回滚继续执行。如果插入失败,则认为 Try 方法正再执行,等待 TC 的重试即可。如果一开始读取事务记录不为空,则说明 Try 方法已经执行完毕,再检查状态是否为初始化,如果是,则还没有执行过其他二阶段方法,正常执行 Cancel 逻辑。如果状态为已回滚,则说明这是重复调用,允许幂等,直接返回成功即可。如果状态为已提交,则同样是一个异常,一个已提交的事务,不能再次回滚。

    TCC 性能优化

    虽然 TCC 模型已经完备,但是随着业务的增长,对于 TCC 模型的挑战也越来越大,可能还需要一些特殊的优化,才能满足业务需求。下面将介绍一下蚂蚁金服内部在 TCC 模型上都做了哪些优化。

    1.同库模式

    第一个优化方案是改为同库模式。同库模式简单来说,就是分支事务记录与业务数据在相同的库中。什么意思呢?之前提到,在注册分支事务记录的时候,框架的调用方切面会先向 TC 注册一个分支事务记录,注册成功后,才会继续往下执行 RPC 调用。TC 在收到分支事务记录注册请求后,会往自己的数据库里插入一条分支事务记录,从而保证事务数据的持久化存储。那同库模式就是调用方切面不再向 TC 注册了,而是直接往业务的数据库里插入一条事务记录。

    在这里插入图片描述

    在讲解同库模式的性能优化点之前,先给大家简单讲讲同库模式的恢复逻辑。一个分布式事务的提交或回滚还是由发起方通知 TC,但是由于分支事务记录保存在业务数据库,而不是 TC 端。因此,TC 不知道有哪些分支事务记录,在收到提交或回滚的通知后,仅仅是记录一下该分布式事务的状态。那分支事务记录怎么真正执行第二阶段呢?需要在各个参与者内部启动一个异步任务,定期捞取业务数据库中未结束的分支事务记录,然后向 TC 检查整个分布式事务的状态,即图中的 StateCheckRequest 请求。TC 在收到这个请求后,会根据之前保存的分布式事务的状态,告诉参与者是提交还是回滚,从而完成分支事务记录。

    在这里插入图片描述

    **那这样做有什么好处呢?**左边是采用同库模式前的调用关系图,在每次调用一个参与者的时候,都是先向 TC 注册一个分布式事务记录,TC 再持久化存储在自己的数据库中,也就是说,一个分支事务记录的注册,包含一次 RPC 和一次持久化存储。

    右边是优化后的调用关系图。从图中可以看出,每次调用一个参与者的时候,都是直接保存在业务的数据库中,从而减少与 TC 之间的 RPC 调用。优化后,有多少个参与者,就节约多少次 RPC 调用。

    这就是同库模式的性能方案。把分支事务记录保存在业务数据库中,从而减少与 TC 的 RPC 调用。

    2. 异步化

    另外一个性能优化方式就是异步化,什么是异步化。TCC 模型的一个作用就是把两阶段拆分成了两个独立的阶段,通过资源业务锁定的方式进行关联。资源业务锁定方式的好处在于,既不会阻塞其他事务在第一阶段对于相同资源的继续使用,也不会影响本事务第二阶段的正确执行。从理论上来说,只要业务允许,事务的第二阶段什么时候执行都可以,反正资源已经业务锁定,不会有其他事务动用该事务锁定的资源。

    假设只有一个中间账户的情况下,每次调用支付服务的 Commit 接口,都会锁定中间账户,中间账户存在热点性能问题。

    但是,在担保交易场景中,七天以后才需要将资金从中间账户划拨给商户,中间账户并不需要对外展示。因此,在执行完支付服务的第一阶段后,就可以认为本次交易的支付环节已经完成,并向用户和商户返回支付成功的结果,并不需要马上执行支付服务二阶段的 Commit 接口,等到低锋期时,再慢慢消化,异步地执行。

    Saga模式

    Saga 模式是 由蚂蚁金服主要贡献的长事务解决方案。在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

    分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
    在这里插入图片描述
    Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。

    Saga 模式使用场景

    适用场景:

    • 业务流程长、业务流程多
    • 参与者包含其他公司或遗留系统服务,无法提供TCC模式要求的三个接口
    • 典型业务系统:如金融网络(与外部金融机构对接)、互联网微贷、渠道整合、分布式架构服务集成等业务系统
    • 银行业金融机构使用广泛

    优势:

    • 一阶段提交本地事务,无锁,高性能
    • 参与者可异步执行,高吞吐
    • 补偿服务易于实现

    缺点:

    • 由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性

    基于状态机引擎的 Saga 实现

    在这里插入图片描述
    目前 Saga 的实现一般有两种,一种是通过事件驱动架构实现,一种是基于注解加拦截器拦截业务的正向服务实现。Seata 目前是采用事件驱动的机制来实现的,Seata 实现了一个状态机,可以编排服务的调用流程及正向服务的补偿服务,生成一个 json 文件定义的状态图,状态机引擎驱动到这个图的运行,当发生异常的时候状态机触发回滚,逐个执行补偿服务。当然在什么情况下触发回滚用户是可以自定义决定的。该状态机可以实现服务编排的需求,它支持单项选择、并发、异步、子状态机调用、参数转换、参数映射、服务执行状态判断、异常捕获等功能。

    状态机引擎原理

    在这里插入图片描述
    该状态机引擎的基本原理是,它基于事件驱动架构,每个步骤都是异步执行的,步骤与步骤之间通过事件队列流转,
    极大的提高系统吞吐量。每个步骤执行时会记录事务日志,用于出现异常时回滚时使用,事务日志会记录在与业务表所在的数据库内,提高性能。

    状态机引擎设计

    在这里插入图片描述
    该状态机引擎分成了三层架构的设计,最底层是“事件驱动”层,实现了 EventBus 和消费事件的线程池,是一个 Pub-Sub 的架构。第二层是“流程控制器”层,它实现了一个极简的流程引擎框架,它驱动一个“空”的流程执行,“空”的意思是指它不关心流程节点做什么事情,它只执行每个节点的 process 方法,然后执行 route 方法流转到下一个节点。这是一个通用框架,基于这两层,开发者可以实现任何流程引擎。最上层是“状态机引擎”层,它实现了每种状态节点的“行为”及“路由”逻辑代码,提供 API 和状态图仓库,同时还有一些其它组件,比如表达式语言、逻辑计算器、流水生成器、拦截器、配置管理、事务日志记录等。

    Saga 服务设计经验

    和TCC类似,Saga的正向服务与反向服务也需求遵循以下设计原则:
    1)Saga 服务设计 - 允许空补偿
    2)Saga 服务设计 - 防悬挂控制
    3)Saga 服务设计 - 幂等控制
    4)Saga 设计 - 自定义事务恢复策略
    前面讲到 Saga 模式不保证事务的隔离性,在极端情况下可能出现脏写。比如在分布式事务未提交的情况下,前一个服务的数据被修改了,而后面的服务发生了异常需要进行回滚,可能由于前面服务的数据被修改后无法进行补偿操作。这时的一种处理办法可以是“重试”继续往前完成这个分布式事务。由于整个业务流程是由状态机编排的,即使是事后恢复也可以继续往前重试。所以用户可以根据业务特点配置该流程的事务处理策略是优先“回滚”还是“重试”,当事务超时的时候,Server 端会根据这个策略不断进行重试。

    由于 Saga 不保证隔离性,所以我们在业务设计的时候需要做到“宁可长款,不可短款”的原则,长款是指在出现差错的时候站在我方的角度钱多了的情况,钱少了则是短款,因为如果长款可以给客户退款,而短款则可能钱追不回来了,也就是说在业务设计的时候,一定是先扣客户帐再入帐,如果因为隔离性问题造成覆盖更新,也不会出现钱少了的情况。

    基于注解和拦截器的 Saga 实现

    在这里插入图片描述
    还有一种 Saga 的实现是基于注解+拦截器的实现,Seata 目前没有实现,可以看上面的伪代码来理解一下,one 方法上定义了 @SagaCompensable 的注解,用于定义 one 方法的补偿方法是 compensateOne 方法。然后在业务流程代码 processA 方法上定义 @SagaTransactional 注解,启动 Saga 分布式事务,通过拦截器拦截每个正向方法当出现异常的时候触发回滚操作,调用正向方法的补偿方法。

    两种 Saga 实现优劣对比

    在这里插入图片描述
    状态机引擎的最大优势是可以通过事件驱动的方法异步执行提高系统吞吐,可以实现服务编排需求,在 Saga 模式缺乏隔离性的情况下,可以多一种“向前重试”的事情恢复策略。注解加拦截器的的最大优势是,开发简单、学习成本低。

    • AT模式,二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。 实际应用中需要关注一下这个问题

    官方文档地址(中文版):http://seata.io/zh-cn
    参考链接:
    https://www.jianshu.com/p/044e95223a17
    https://www.jianshu.com/p/fe8c48f38382
    https://blog.csdn.net/huaishu/article/details/89880971

    展开全文
  • Java JDBC事务与JTA分布式事务

    千次阅读 2016-08-19 17:02:04
    Java事务的类型有三种:JDBC事务、JTA(Java Transaction API)事务、容器事务。 常见的容器事务如Spring事务,容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API...

    Java事务的类型有三种:JDBC事务JTA(Java Transaction API)事务容器事务。 常见的容器事务如Spring事务,容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API实现。所以本文暂不讨论容器事务。本文主要介绍J2EE开发中两个比较基本的事务:JDBC事务JTA事务

    JDBC事务

    JDBC的一切行为包括事务是基于一个Connection的,在JDBC中是通过Connection对象进行事务管理。在JDBC中,常用的和事务相关的方法是: setAutoCommitcommitrollback等。


    下面看一个简单的JDBC事务代码:

    public void JdbcTransfer() { 
        java.sql.Connection conn = null;
         try{ 
            conn = conn =DriverManager.getConnection("jdbc:oracle:thin:@host:1521:SID","username","userpwd");
             // 将自动提交设置为 false,
             //若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
             conn.setAutoCommit(false);
    
             stmt = conn.createStatement(); 
             // 将 A 账户中的金额减少 500 
             stmt.execute("\
             update t_account set amount = amount - 500 where account_id = 'A'");
             // 将 B 账户中的金额增加 500 
             stmt.execute("\
             update t_account set amount = amount + 500 where account_id = 'B'");
    
             // 提交事务
             conn.commit();
             // 事务提交:转账的两步操作同时成功
         } catch(SQLException sqle){            
             try{ 
                 // 发生异常,回滚在本事务中的操做
                conn.rollback();
                 // 事务回滚:转账的两步操作完全撤销
                 stmt.close(); 
                 conn.close(); 
             }catch(Exception ignore){ 
    
             } 
             sqle.printStackTrace(); 
         } 
    }

    上面的代码实现了一个简单的转账功能,通过事务来控制转账操作,要么都提交,要么都回滚。

    JDBC事务的优缺点

    JDBC为使用Java进行数据库的事务操作提供了最基本的支持。通过JDBC事务,我们可以将多个SQL语句放到同一个事务中,保证其ACID特性。JDBC事务的主要优点就是API比较简单,可以实现最基本的事务操作,性能也相对较好。

    但是,JDBC事务有一个局限:一个 JDBC 事务不能跨越多个数据库!!!所以,如果涉及到多数据库的操作或者分布式场景,JDBC事务就无能为力了。

    JTA事务

    为什么需要JTA

    通常,JDBC事务就可以解决数据的一致性等问题,鉴于他用法相对简单,所以很多人关于Java中的事务只知道有JDBC事务,或者有人知道框架中的事务(比如Hibernate、Spring)等。但是,由于JDBC无法实现分布式事务,而如今的分布式场景越来越多,所以,JTA事务就应运而生。

    如果,你在工作中没有遇到JDBC事务无法解决的场景,那么只能说你做的项目还都太小。拿电商网站来说,我们一般把一个电商网站横向拆分成商品模块、订单模块、购物车模块、消息模块、支付模块等。然后我们把不同的模块部署到不同的机器上,各个模块之间通过远程服务调用(RPC)等方式进行通信。以一个分布式的系统对外提供服务。

    一个支付流程就要和多个模块进行交互,每个模块都部署在不同的机器中,并且每个模块操作的数据库都不一致,这时候就无法使用JDBC来管理事务。我们看一段代码:

    /** 支付订单处理 **/
    @Transactional(rollbackFor = Exception.class)
    public void completeOrder() {
        orderDao.update(); // 订单服务本地更新订单状态
        accountService.update(); // 调用资金账户服务给资金帐户加款
        pointService.update(); // 调用积分服务给积分帐户增加积分
        accountingService.insert(); // 调用会计服务向会计系统写入会计原始凭证
        merchantNotifyService.notify(); // 调用商户通知服务向商户发送支付结果通知
    }

    上面的代码是一个简单的支付流程的操作,其中调用了五个服务,这五个服务都通过RPC的方式调用,请问使用JDBC如何保证事务一致性?我在方法中增加了@Transactional注解,但是由于采用调用了分布式服务,该事务并不能达到ACID的效果。

    JTA事务比JDBC事务更强大。一个JTA事务可以有多个参与者,而一个JDBC事务则被限定在一个单一的数据库连接。下列任一个Java平台的组件都可以参与到一个JTA事务中:JDBC连接、JDO PersistenceManager 对象、JMS 队列、JMS 主题、企业JavaBeans(EJB)、一个用J2EE Connector Architecture 规范编译的资源分配器。

    JTA的定义

    Java事务API(Java Transaction API,简称JTA ) 是一个Java企业版 的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。


    JTA和它的同胞Java事务服务(JTS;Java TransactionService),为J2EE平台提供了分布式事务服务。不过JTA只是提供了一个接口,并没有提供具体的实现,而是由j2ee服务器提供商 根据JTS规范提供的,常见的JTA实现有以下几种:

    • 1.J2EE容器所提供的JTA实现(JBoss)
    • 2.独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。

    JTA里面提供了 java.transaction.UserTransaction ,里面定义了下面几个方法

    begin:开启一个事务

    commit:提交当前事务

    rollback:回滚当前事务

    setRollbackOnly:把当前事务标记为回滚

    setTransactionTimeout:设置事务的事件,超过这个事件,就抛出异常,回滚事务

    这里,值得注意的是,不是使用了UserTransaction就能把普通的JDBC操作直接转成JTA操作,JTA对DataSource、Connection和Resource 都是有要求的,只有符合XA规范,并且实现了XA规范的相关接口的类才能参与到JTA事务中来,关于XA规范,请看我的另外一篇文章中有相关介绍。这里,提一句,目前主流的数据库都支持XA规范。

    要想使用用 JTA 事务,那么就需要有一个实现 javax.sql.XADataSourcejavax.sql.XAConnectionjavax.sql.XAResource 接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个 XADataSource 对象就是一个 XAConnection 对象的工厂。XAConnection 是参与 JTA 事务的 JDBC 连接。

    要使用JTA事务,必须使用XADataSource来产生数据库连接,产生的连接为一个XA连接。

    XA连接(javax.sql.XAConnection)和非XA(java.sql.Connection)连接的区别在于:XA可以参与JTA的事务,而且不支持自动提交。


    示例代码:

    public void JtaTransfer() { 
            javax.transaction.UserTransaction tx = null;
            java.sql.Connection conn = null;
             try{ 
                 tx = (javax.transaction.UserTransaction) context.lookup("java:comp/UserTransaction");  //取得JTA事务,本例中是由Jboss容器管理
                 javax.sql.DataSource ds = (javax.sql.DataSource) context.lookup("java:/XAOracleDS");  //取得数据库连接池,必须有支持XA的数据库、驱动程序  
                 tx.begin();
                conn = ds.getConnection();
    
                 // 将自动提交设置为 false,
                 //若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
                 conn.setAutoCommit(false);
    
                 stmt = conn.createStatement(); 
                 // 将 A 账户中的金额减少 500 
                 stmt.execute("\
                 update t_account set amount = amount - 500 where account_id = 'A'");
                 // 将 B 账户中的金额增加 500 
                 stmt.execute("\
                 update t_account set amount = amount + 500 where account_id = 'B'");
    
                 // 提交事务
                 tx.commit();
                 // 事务提交:转账的两步操作同时成功
             } catch(SQLException sqle){            
                 try{ 
                     // 发生异常,回滚在本事务中的操做
                  tx.rollback();
                     // 事务回滚:转账的两步操作完全撤销
                     stmt.close(); 
                     conn.close(); 
                 }catch(Exception ignore){ 
    
                 } 
                 sqle.printStackTrace(); 
             } 
         }

    上面的例子就是一个使用JTA事务的转账操作,该操作相对依赖于J2EE容器,并且需要通过JNDI的方式获取 UserTransactionConnection

    标准的分布式事务

    一个分布式事务(Distributed Transaction)包括一个 事务管理器transaction manager)和一个或多个 资源管理器( resource manager)。一个 资源管理器resource manager)是任意类型的持久化数据存储。 事务管理器transaction manager)承担着所有事务参与单元者的相互通讯的责任。

    JTA的实现方式也是基于以上这些分布式事务参与者实现的,具体的关于JTA的实现细节不是本文的重点,感兴趣的同学可以阅读JTA 深度历险 – 原理与实现

    • 看上面关于分布式事务的介绍是不是和2PC中的事务管理比较像?的却,2PC其实就是符合XA规范的事务管理器协调多个资源管理器的一种实现方式。 我之前有几篇文章关于2PC和3PC的,那几篇文章中介绍过分布式事务中的事务管理器是如何协调多个事务的统一提交或回滚的,后面我还会有几篇文章详细的介绍一下和分布式事务相关的内容,包括但不限于全局事务、DTP模型、柔性事务等。

    JTA的优缺点

    JTA的优点很明显,就是提供了分布式事务的解决方案,严格的ACID。但是,标准的JTA方式的事务管理在日常开发中并不常用,因为他有很多缺点:

    • 实现复杂
      • 通常情况下,JTA UserTransaction需要从JNDI获取。这意味着,如果我们使用JTA,就需要同时使用JTA和JNDI。
    • JTA本身就是个笨重的API
    • 通常JTA只能在应用服务器环境下使用,因此使用JTA会限制代码的复用性。

    总结

    Java事务的类型有三种:JDBC事务JTA(Java Transaction API)事务容器事务,其中JDBC的事务操作用法比较简单,适合于处理同一个数据源的操作。JTA事务相对复杂,可以用于处理跨多个数据库的事务,是分布式事务的一种解决方案。

    这里还要简单说一下,虽然JTA事务是Java提供的可用于分布式事务的一套API,但是不同的J2EE平台的实现都不一样,并且都不是很方便使用,所以,一般在项目中不太使用这种较为负责的API。现在业内比较常用的分布式事务解决方案主要有异步消息确保型、TCC、最大努力通知等。关于这几种分布式事务解决方案,我会在后面的文章中介绍。欢迎关注与交流。

    参考资料

    JTA 深度历险 – 原理与实现

    事务模型与分布式事务总结思考


    文章来源:http://www.hollischuang.com/archives/1658

    展开全文
  • 什么事务? 本地事务 事务传播行为: 事务的四大特性 ACID 并发事务产生的问题可以分为4类 事务的隔离级别 什么是分布式事务 分布式涉及到的原理: CAP原理: BASE理论 柔性事务和刚性事务 柔性事务分为:...

    目录

    什么是事务?

    本地事务

    事务传播行为:

    事务的四大特性 ACID

    并发事务产生的问题可以分为4类

    事务的隔离级别

    什么是分布式事务

    分布式涉及到的原理:

    CAP原理:

    BASE理论

    柔性事务和刚性事务

    柔性事务分为:

    分布式一致性协议

    XA接口      

    Jta规范

    两阶段提交协议 2PC

    三阶段提交协议 3PC

    XA与TCC 的比较:

    分布式事务解决方案

    方案1:全局事务(DTP模型)

    分布式跨库事务:

    方案2 .TCC 两阶段补偿方案(Try -- Confirm/Cancel)

    方案3 .可靠消息最终一致性

    方案4 .最大努力通知方案:


     


    什么是事务?

    事务由一组指令操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作要么全都正确执行要么全都不要执行

    方案:

    悲观锁 for update

    乐观锁 version 字段版本控制

     

     @Autowired
        private TransactionTemplate txTemplate;
    
        public String sendOrderByTxTemplate(Order order) {
            Long orderId = order.getId();
            Boolean lockStatus = txTemplate.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus transactionStatus) {
                    Order order = new Order();
                    order.setId(orderId);
                    order.setOrderStatus("4");//处理中...
                    order.setVersion(0);
                    return 1 == orderMapper.updateByVersion(order);
                }
            });
    
            //j只有第一个进来的线程返回true
            //基于状态机的乐观锁..
            if (lockStatus) {//只允许第一个线程调用发货接口
                String flag = transService.invoke(url, orderId);//10s
                //只作用于代码块<===
                txTemplate.execute(new TransactionCallback<Boolean>() {
                    @Override
                    public Boolean doInTransaction(TransactionStatus transactionStatus) {
                        Order orderFin = new Order();
                        orderFin.setId(orderId);
                        orderFin.setOrderStatus(flag);//处理中...
                        orderMapper.update(orderFin);
                        return null;
                    }
                });
            }

    本地事务

    众所周知,数据库实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务

    事务传播行为:

    事务的四大特性 ACID

    我们首先看下一些书籍中的官方描述:

    数据库:

    原子性(Atomicity) 
    原子性要求: 事务是一个不可分割的执行单元,事务中的所有操作要么全都正确执行,要么全都不执行。

    一致性 (Consistency)
    一致性要求: 事务在开始前结束后,数据库的完整性约束没有被破坏

    隔离性 (Isolation)
    隔离性要求: 事务的执行是相互独立的,它们不会相互干扰,一个事务不会看另一个正在运行过程中的事务的数据

    持久性(Durability) 
    持久性要求: 一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失

    注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。

    --《Spring攻略》

    • 原子性(Atomictiy):事务是一个包含一系列操作的原子操作。事务的原子性确保这些操作全部完成或者全部失败。
    • 一致性(Consistency):一旦事务的所有操作结束,事务就被提交。然后你的数据和资源将处于遵循业务规则的一直状态。
    • 隔离性(Isolation):因为同时在相同数据集上可能有许多事务处理,每个事务应该与其他事务隔离,避免数据破坏。
    • 持久性(Durability):一旦事务完成,他的结果应该能够承受任何系统错误(想象一下在事务提交过程中机器的电源被切断的情况)。通常,事务的结果被写入持续性存储。

     

    --《企业应用架构模式》

    • 原子性(Atomictiy):在一个事务里,动作序列的每一个步骤都必须是要么全部成功,要么所有的工作都将回滚。部分完成不是一个事务的概念。
    • 一致性(Consistency):在事务开始和完成的时候,系统的资源都必须处于一致的、没有被破坏的状态。
    • 隔离性(Isolation):一个事务,直到它被成功提交之后,它的结果对于任何其他的事务才是可见的。
    • 持久性(Durability):一个已提交事务的任何结果都必须是永久性的,即“在任何系统崩溃的情况下都能保存下来”。

    那这四个属性,我们自己到底该如何理解呢。个人理解如下:
    如果给事务下一个定义:事务是一个有边界的指令操作序列,开始和结束都有明确的定义。

    1. 原子性
      举例1-比如现在有一个事务,包含3个sql语句(工作序列,或者是指令序列),sql-1,sql-2,sql-3,这3个sql语句,每一个在执行的时候,都是一个单元,这个单元的执行结果,有且仅有两种可能:成功和失败。
      举例2-再比如,我从账户1中转出1000人民币到账户2,当我从账户1中把钱转出来之后,系统就崩溃了。那么系统应该将我的账户状态置成我还没有转出钱之前的状态。
    2. 一致性
      举例1-有一个在线商务网站系统,有两张表,一张用户账户表(用户名、个人余额),一张商品库存表(商品ID,库存数量),用户花费30元购买1件商品,商品库存减1,账户余额减30.那么这样的结果就是一致的。否则,如果商品库存减1,账户余额没有变化,那么这样的结果就是不一致的。
      举例2-银行账户转账的例子,也一样,账户1中的钱减少1000,账户2中的钱就增加1000,这样的是一致的,否则不一致。
    3. 隔离性
      没有隔离性就没有一致性
      举例1-我们现在有两个事务方法,一个方法是查询数据的库存,一个是购买下单,那么这两个事务方法应该互不影响,不然会造成一系列的问题,个人一直认为,事务造成的下面这些问题是跟并发密不可分的,没有并发操作,单一的请求事务是不会有这样的问题的。

      并发事务产生的问题可以分为4类

      我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

      脏读  (我未提交你就读)
      事务1读到事务2尚未提交un-commit的事务中的数据。该数据可能会被回滚从而失效。 
      如果事务1拿着失效的数据去处理那就发生错误了。

      更新丢失  (我改你也改)
      当有两个并发执行的事务更新同一行数据,那么有可能一个事务会把另一个事务更新覆盖掉。 
      当数据库没有加任何锁操作的情况下会发生

      不可重复读 (我读中你捣乱[2])
      不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

      虚读:在事务1两次读取同一记录过程中事务2该记录进行修改操作,从而事务1第二次读到了不一样记录
      幻读事务1两次查询的过程中事务2该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化
       

      不可重复读 与 脏读 的区别? 
      脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。

      事务的隔离级别

      对事务的隔离性做一个详细的解释。

      在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间互不干扰。这确实能够完全保证数据的安全性实际业务系统中,这种方式性能不高因此,数据库定义了四种隔离级别隔离级别和数据库的性能呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差

      数据库的四种隔离级别
      数据库一共有如下四种隔离级别:

      从低到高

      Read uncommitted 读未提交  (①我写未完成时,你禁写可读; ②我读时,你可写可读)
      在该级别下,一个事务一行数据修改过程中不允许另一个事务该行数据进行修改操作但允许该行数据进行读操作。 
      因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读(虚读/幻读)问题。

      Read committed 读已提交  (①我写时未提交,你禁写读; 我读时,可写可读,[不管[爱咋咋地],想读写都可以])
      在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此出现不可重复读(虚读/幻读)的情况。

      Repeatable read 可重复读  (①我写时,你禁写读; ②我读时,你禁写可读) [MySQL默认隔离级别:可重复读]
      在该级别下,读事务禁止写事务允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读(虚读/幻读)),且写事务禁止其他一切事务

      Serializable 序列化(串行化)  (①我写时和②我读时,你禁止一切)
      该级别要求所有事务必须串行执行,因此能避免一切并发引起的问题,但效率很低

      隔离级别越高,越能保证数据完整性和一致性是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁乐观锁来控制。

      在《企业应用架构模式》一书中,有一句话描述就是:处理并发最主要的工具就是事务。

    4. 持久性
      对于数据库来讲,我的理解是这样,当我把sql-1、sql-2、sql-3提交之后,这个结果就一定会保存到数据库中,那如果提交-到写入这中间,突然断电,也没有关系,数据库服务器在重新启动之后一样会把数据写入磁盘,应该是通过日志的方式-仅个人理解。
      数据库一般都是通过事务日志的方式,write-ahead transaction log来保证持久性。write-ahead transaction log的意思是,事务中对数据库的改变在写入到数据库之前,首先写入到事务日志中。而事务日志是按照顺序排号的(LSN)。当数据库崩溃或者服务器断点时,重启动数据库,首先会检查日志顺序号,将本应对数据库做更改而未做的部分持久化到数据库,从而保证了持久性.

    什么是分布式事务

    • 分布式事务就是指事务的资源分别位于不同的分布式系统的不同节点之上的事务
    • 上面所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现跨数据库事务支持,这也就是大家常说的“分布式事务”。
    • 分布式事务涉及到操作多个数据库的事务,分布式事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点上。一个分布式事务可以看作是由多个分布式的操作序列组成的,通常可以把这一系列分布式的操作序列称为子事务,但由于在分布式事务中,各个子事务的执行是分布式的,因此要实现一种能够保证 ACID 特性的分布式事务处理系统就显得格外复杂。

    分布式事务产生的原因

    • 数据库分库分表

    ​    在单库单表场景下,当业务数据量达到单库单表的极限时,就需要考虑分库分表,将之前的单库单表拆分成多库多表;分库分表之后,原来在单个数据库上的事务操作,可能就变成跨多个数据库的操作,此时就需要使用分布式事务。

    • 业务服务化

    ​    业务服务化即业务按照面向服务(SOA)的架构拆分整个网站系统,所有的业务操作都以服务的方式对外发布,跨应用、跨服务的操作需要使用分布式事务才能保证数据的一致性。

     

    这里举一个分布式事务的典型例子——用户下单过程。 
    当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统.仓储服务等。整个下单的过程如下:

    用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
    此时订单系统会生成一条订单
    订单创建成功后,支付系统提供支付功能
    当支付完成后,由积分系统为该用户增加积分
    上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。

    分布式涉及到的原理:

    CAP原理:

      

     由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系统的CAP原理包含如下三个元素:

    概述
      分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。

    Consistency 一致性
      一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。

    Availability 可用性
      可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

    Parttition Tolerance 分区容忍性
      网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。

    权衡
      在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
    为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
    为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
    通常采取的策略是保证可用性,牺牲部分一致性,只确保最终一致性。

           当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间。

      

    BASE理论


            BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。

    1、基本可用  BA:(Basically Available ):

           指分布式系统在出现故障的时候,允许损失部分可用性保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。简单来说就是基本可用

    2、软状态  S:( Soft State):

            软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。简单来说就是状态可以在一段时间内不同步

    3、最终一致性  E:(Eventually Consistent ):

           系统中的所有数据副本经过一定时间后,最终能够达到一致的状态不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。简单来说就是在一定的时间窗口内, 最终数据达成一致即可。

    柔性事务和刚性事务

     

    1. 刚性事务满足ACID理论
    2. 柔性事务满足BASE理论(基本可用,最终一致)

    柔性事务分为:

    两阶段型 2PC(two phase commitment)
    补偿型TCC (try confirn/caclen
    异步确保型 (通过信息中间件)
    最大努力通知型。
     

    酸碱平衡
    ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。

    分布式一致性协议


    XA接口
          

    XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调事务管理器控制着全局事务,管理事务生命周期,并协调资源资源管理器负责控制和管理实际资源(如数据库或JMS队列)

    Jta规范


           作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:

    1.J2EE容器所提供的JTA实现(JBoss)
    2.独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里
    用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。

    两阶段提交协议 2PC

    两阶段提交协议 2PC
    分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

    该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
    所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失
    所有节点不会永久性损坏,即使损坏后仍然可以恢复


    1. 第一阶段(投票(准备)阶段)

    协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
    参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
    各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
    2. 第二阶段(提交执行阶段)

    当协调者节点从所有参与者节点获得的相应消息都为”同意”时:

    协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
    参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
    参与者节点向协调者节点发送”完成”消息。
    协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
    如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

    协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
    参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
    参与者节点向协调者节点发送”回滚完成”消息。
    协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
    不管最后结果如何,第二阶段都会结束当前事务。

    二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

    执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
    参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
    协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)
    二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
    为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。

    三阶段提交协议 3PC


    与两阶段提交不同的是,三阶段提交有两个改动点。

    引入超时机制。同时在协调者参与者中都引入超时机制
    在第一阶段和第二阶段中插入一个准备阶段保证了在最后提交阶段之前各参与节点状态一致的。
    也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

    1. CanCommit阶段

    3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

    事务询问 
    协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
    响应反馈 
    参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
    2. PreCommit阶段

    协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。 
    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

    发送预提交请求 
    协调者向参与者发送PreCommit请求,并进入Prepared阶段。

    事务预提交 
    参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

    响应反馈 
    如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

    假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

    发送中断请求 
    协调者向所有参与者发送abort请求。

    中断事务 
    参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

    3. doCommit阶段 
    该阶段进行真正的事务提交,也可以分为以下两种情况。

    • 3.1 执行提交

    发送提交请求 
    协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    事务提交 
    参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    响应反馈 
    事务提交完之后,向协调者发送Ack响应。
    完成事务 
    协调者接收到所有参与者的ack响应之后,完成事务。

    • 3.2 中断事务 

    协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

    发送中断请求 
    协调者向所有参与者发送abort请求

    事务回滚 
    参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

    反馈结果 
    参与者完成事务回滚之后,向协调者发送ACK消息

    中断事务 
    协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

     

    2PC与3PC提交区别
    增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大。
    三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。

     

    XA与TCC 的比较:

     

    分布式事务解决方案

     

    方案1:全局事务(DTP模型)


    全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色:

    AP:Application 应用系统 
    它就是我们开发的业务系统,在我们开发的过程中,可以使用资源管理器提供的事务接口来实现分布式事务。

    TM:Transaction Manager 事务管理器

    分布式事务的实现由事务管理器来完成,它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。
    事务管理器还管理着所有的资源管理器,通过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。
    DTP只是一套实现分布式事务的规范,并没有定义具体如何实现分布式事务,TM可以采用2PC、3PC、Paxos等协议实现分布式事务。
    RM:Resource Manager 资源管理器

    能够提供数据服务的对象都可以是资源管理器,比如:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。
    资源管理器能够提供单数据库的事务能力,它们通过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。
    XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。
    DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。
    有没有基于DTP模型的分布式事务中间件?

    参考文献
    大规模SOA系统中的分布事务处理_程立
    Life beyond Distributed Transactions: an Apostate’s Opinion
    关于如何实现一个TCC分布式事务框架的一点思考
    How can a requestor ensure a consistent outcome across multiple, independent providers
    关于分布式事务、两阶段提交协议、三阶提交协议
    Three-phase commit protocol

    分布式跨库事务:

    pom依赖:

    DataSource:

    事务过程:

    对atomikos的最终提交commit的模拟过程:

     

    方案2 .TCC 两阶段补偿方案(Try -- Confirm/Cancel)


     

    TCC即为Try Confirm Cancel,它属于补偿型分布式事务。顾名思义,TCC实现分布式事务一共有2个步骤:

     

    1 预留资源阶段

    Try:尝试待执行的业务 
    这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源

    2 确认资源阶段

    • Confirm:执行业务 

    这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,

    而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。

    • Cancel:取消执行的业务 

    若业务执行失败,则进入Cancel阶段,它会释放所有占用的业务资源,并回滚Confirm阶段执行的操作。

    TCC原理:

     

    案例:

    优缺点及使用场景:

    TCC框架:

    TCC全局事务必须基于RM本地事务来实现全局事务
    TCC服务是由Try -- Confirm/Cancel业务构成的, 
    其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操作,必须要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。

     

    再考虑一下如下场景:A==>B转账,余额系统和红包系统是两个独立的系统

    假设服务B没有基于RM本地事务(以RDBS为例,可通过设置auto-commit为true来模拟),那么一旦[B:Try]操作中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则需要判断[B:Try]中哪些操作已经写到DB、哪些操作还没有写到DB:假设[B:Try]业务有5个写库操作,[B:Cancel]业务则需要逐个判断这5个操作是否生效,并将生效的操作执行反向操作。

    不幸的是,由于[B:Cancel]业务也有n(0<=n<=5)个反向的写库操作,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。因为,相比第一次[B:Cancel]操作,后续的[B:Cancel]操作还需要判断先前的[B:Cancel]操作的n(0<=n<=5)个写库中哪几个已经执行、哪几个还没有执行,这就涉及到了幂等性问题。而对幂等性的保障,又很可能还需要涉及额外的写库操作,该写库操作又会因为没有RM本地事务的支持而存在类似问题。。。可想而知,如果不基于RM本地事务,TCC事务框架是无法有效的管理TCC全局事务的。

    反之,基于RM本地事务的TCC事务,这种情况则会很容易处理:[B:Try]操作中途执行失败,TCC事务框架将其参与RM本地事务直接rollback即可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操作涉及的RM本地事务已经rollback”的情况下,根本无需执行[B:Cancel]操作。

    换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不需要考虑部分执行的情况。

    TCC事务框架应该提供Confirm/Cancel服务的幂等性保障
    一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用

    在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多。比如,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。因此,故障恢复机制后续仍然会重新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。

    既然Confirm/Cancel业务可能会被多次调用,就需要保障其幂等性。 
    那么,应该由TCC事务框架来提供幂等性保障?还是应该由业务系统自行来保障幂等性呢? 
    个人认为,应该是由TCC事务框架来提供幂等性保障。如果仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是可以的;然而,这是一类公共问题,毫无疑问,所有TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;而且,考虑一下由业务系统来负责幂等性需要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。

     

    方案3 .可靠消息最终一致性

    1 . 普通消息队列中间件

    2 . 基于RabbitMQ/RocketMQ消息中间件

    这种实现分布式事务的方式需要通过消息中间件来实现。假设有A和B两个系统,分别可以处理任务A和任务B。此时系统A中存在一个业务流程,需要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。

    在系统A处理任务A前,首先向消息中间件发送一条消息
    消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。
    消息中间件持久化成功后,便向系统A返回一个确认应答;
    系统A收到确认应答后,则可以开始处理任务A;
    任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。 
    但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。
    消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;
    当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。
    上述过程可以得出如下几个结论: 
    1. 消息中间件扮演者分布式事务协调者的角色。 
    2. 系统A完成任务A后,到任务B执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足BASE理论。

    上述过程中,如果任务A处理失败,那么需要进入回滚流程,如下图所示: 


    若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。和发送Commit请求一样,系统A发完之后便可以认为回滚已经完成,它便可以去做其他的事情。
    消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统B,从而不会触发系统B的任务B。
    此时系统又处于一致性状态,因为任务A和任务B都没有执行。

    上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。

    系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果:

    提交 
    若获得的状态是“提交”,则将该消息投递给系统B。
    回滚 
    若获得的状态是“回滚”,则直接将条消息丢弃。
    处理中 
    若获得的状态是“处理中”,则继续等待。


    消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况,而且能降低上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后便可以处理其他任务,无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补,这样大大降低上游系统的阻塞时间,提升系统的并发度。

    下面来说一说消息投递过程的可靠性保证。 
    当上游系统执行完任务并向消息中间件提交了Commit指令后,便可以处理其他任务了,此时它可以认为事务已经完成,接下来消息中间件一定会保证消息被下游系统成功消费掉!那么这是怎么做到的呢?这由消息中间件的投递流程来保证。

    消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕!

    如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。当然,一般消息中间件可以设置消息重试的次数和时间间隔,比如:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。如果重试3次之后仍然投递失败,那么这条消息就需要人工干预。 


    有的同学可能要问:消息投递失败后为什么不回滚消息,而是不断尝试重新投递

    这就涉及到整套分布式事务系统的实现成本问题。 
    我们知道,当系统A将向消息中间件发送Commit指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统A事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本。

    不知大家是否发现,上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式,也就是当上游系统提交完消息后便可以去做别的事情,接下来提交、回滚就完全交给消息中间件来完成,并且完全信任消息中间件,认为它一定能正确地完成事务的提交或回滚。然而,消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后,它会阻塞等待,等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为什么这两者在设计上是不一致的呢?

    首先,上游系统消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道,用户体验尤为重要,因此这种异步通信方式能够极大程度地降低用户等待时间。此外,异步通信相对于同步通信而言,没有了长时间的阻塞等待,因此系统的并发性也大大增加。但异步通信可能会引起Commit/Rollback指令丢失的问题,这就由消息中间件的超时询问机制来弥补。

    那么,消息中间件下游系统之间为什么要采用同步通信呢?

    异步提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。 
    我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的BASE理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。

    方案4 .最大努力通知方案:

    最大努力通知也被称为定期校对,其实在方案二中已经包含,这里再单独介绍,主要是为了知识体系的完整性。这种方案也需要消息中间件的参与,其过程如下:

    上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,然后上游系统可以去做别的事情了;
    消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;
    当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。
    上面是一个理想化的过程,但在实际场景中,往往会出现如下几种意外情况:

    消息中间件向下游系统投递消息失败
    上游系统向消息中间件发送消息失败
    对于第一种情况,消息中间件具有重试机制,我们可以在消息中间件中设置消息的重试次数和重试时间间隔,对于网络不稳定导致的消息投递失败的情况,往往重试几次后消息便可以成功投递,如果超过了重试的上限仍然投递失败,那么消息中间件不再投递该消息,而是记录在失败消息表中,消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费,这就是所谓的“定期校对”。

    如果重复投递和定期校对都不能解决问题,往往是因为下游系统出现了严重的错误,此时就需要人工干预。

    对于第二种情况,需要在上游系统中建立消息重发机制。可以在上游系统建立一张本地消息表,并将 任务处理过程 和 向本地消息表中插入消息 这两个步骤放在一个本地事务中完成。如果向本地消息表插入消息失败,那么就会触发回滚,之前的任务处理结果就会被取消。如果这量步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,如果发送失败它会返回重试。当然,也要给消息发送者设置重试的上限,一般而言,达到重试上限仍然发送失败,那就意味着消息中间件出现严重的问题,此时也只有人工干预才能解决问题。

    对于不支持事务型消息的消息中间件,如果要实现分布式事务的话,就可以采用这种方式。它能够通过重试机制+定期校对实现分布式事务,但相比于第二种方案,它达到数据一致性的周期较长,而且还需要在上游系统中实现消息重试发布机制,以确保消息成功发布给消息中间件,这无疑增加了业务系统的开发成本,使得业务系统不够纯粹,并且这些额外的业务逻辑无疑会占用业务系统的硬件资源,从而影响性能。

    因此,尽量选择支持事务型消息的消息中间件来实现分布式事务,如RocketMQ/RabbitMQ。

    COOKIE方面:

    展开全文
  • 分布式事务

    千次阅读 2021-04-07 21:39:07
    分布式事务
  • 分布式事务入门

    万次阅读 2020-04-22 11:48:14
    目录前言问题描述什么是分布式事务什么是分布式系统什么是事务什么是本地事务什么是分布式事务分布式事务的应用场景CAP理论分布式系统能否兼顾C、A、P?CAP有哪些组合方式?分布式事务的解决方案(介绍其中三种)两...
  • 事务 | Spring Cloud 分布式事务管理 LCN、GTX、ByteTCC

    万次阅读 多人点赞 2018-03-18 21:03:04
    事务 | Spring Cloud 分布式事务管理 LCN、GTX、ByteTCC 在微服务如火如荼的情况下,越来越多的项目开始尝试改造成微服务架构,微服务即带来了项目开发的方便性,又提高了运维难度以及网络不可靠的概率. Spring ...
  • 分布式事务——单机事务拾遗

    千次阅读 2017-05-11 22:22:55
    尽管看起来计算机可以并行处理很多事情,但实际上每个CPU单位时间内只能一件事,要么读取数据、要么计算数据、要么写入数据,所有的任务都可以看成这三件事的集合。计算机的这种特性引出了一个问题:当多个人去读...
  • 什么事务2.本地事务2.1事务的4个特性“ACID”3.undo和redo4.分布式事务4.1跨数据源的分布式事务4.2跨服务的分布式事务4.3分布式系统存在的数据一致性的问题5.解决分布式事务的思路5.1CAP定理5.2Base理论6.分阶段...
  • 分布式事务(一)

    千次阅读 2018-08-27 18:02:51
    一、数据库事务 在介绍分布式事务之前,先简单了解下数据库事务。 数据库事务(Database Transaction) :是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务具有ACID四个特性: ...
  • 分布式事务详解

    千次阅读 2019-09-10 20:01:05
    文章目录数据库事务分布式理论CAP定理BASE理论什么是分布式事务分布式事务的产生的原因数据库分库分表应用SOA化事务的ACID特性原子性(A)一致性(C)隔离性(I)持久性(D)分布式事务的应用场景支付在线下单常见的...
  • Java微服务下的分布式事务介绍及其解决方案

    万次阅读 多人点赞 2019-03-22 15:48:46
    1.前言 ...这就很尴尬了,当然微服务下可能没有分布式事务,但是很多场景是需要分布式事务的,下面我就来介绍下什么是分布式事务,和分布式事务的解决方案 2 问题描述 在介绍分布式事务...
  • 事务】——分布式事务

    千次阅读 2018-08-26 10:48:02
    什么会出现这种情况?怎样才能避免呢?有了事务就能解决上述问题。 事务定义 事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况...
  • 这是一个开撕的话题,我经历过太多的关于分布式事务的需求:“有没有简单的方案,像使用数据库事务那样,解决分布式数据一致性的问题”。特别是微服务架构流行的今天,一次交易需要跨越多个“服务”、多个数据库来...
  • es不支持事务什么好的弥补方案吗? 2、事务的核心概念 如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下ACID四个特性: 原子性(Atomicity) 原子性是指事务包含的所有操作要么全部成功,...
  • 深入理解分布式事务,高并发下分布式事务的解决方案
  • 分布式事务的学习

    万次阅读 2020-04-07 09:54:43
    1.什么是分布式事务 要了解分布式事务,必须先了解本地事务。 1.1.本地事务 本地事务,是指传统的单机数据库事务,必须具备ACID原则: 原子性(A) 所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,...
  • 1.1 什么事务 什么事务?举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是一个事务的例子,交钱和交货必 须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。 明白上述...
  • Spring的事务机制

    千次阅读 2018-04-13 16:31:04
    Spring 没有直接提供事务的直接支持,它对外提供统一的 API,真正的事务提供者是实现了这一套 API 的 ORM 框架或者是其他支持事务的框架,例如 Hibernate、Mybatis 等等。 &amp;nbsp;&amp;nbsp;&amp;...
  • 微服务:分布式事务

    千次阅读 多人点赞 2019-09-02 18:16:19
    课程分享: 课程分享:Docker+...这篇文章将介绍什么是分布式事务,分布式事务解决什么问题,对分布式事务实现的难点,解决思路,不同场景下方案的选择,通过图解的方式进行梳理、总结和比较。 相信耐心看...
  • 为了简化这一过程,GPU事务性内存实现了复杂的数据同步和并行,对外则仅提供简单的API。首先介绍了GPU事务性内存的研究背景。其次,讨论了近年的GPU事务性内存的设计方案与策略,分析了不同设计方案遇到的问题和解决...
  • 如何实现一个TCC分布式事务框架

    千次阅读 2019-01-10 16:20:37
    声明:本文转载自:TCC分布式事务框架的一点思考 关于TCC事务机制的介绍,可以参考TCC事务机制简介。 TCC事务模型虽然说起来简单,然而要基于TCC实现一个通用的分布式事务框架,却比它看上去要复杂的多,不只是...
  • 分布式-分布式事务

    万次阅读 2018-07-22 18:04:00
    在单个数据库实例时候,我们可以在一个数据源的事务(本地事务)内多步数据库操作,在事务内的多个操作要么全部执行生效,要么全部不生效。在多数据实例节点时候,我们对多个实例的数据源进行操作时候就没办法把多...
  • 一、数据库事务 1、事务是作为单个逻辑工作单元执行的一系列操作。可以是一条SQL语句也可以是多条SQL语句。 2、事务具有四个特性  原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,...
  • 阿里巴巴中台战略--事务与柔性事务

    千次阅读 2019-05-12 16:34:28
    阿里巴巴中台战略 阿里共享事业部的产生、...不管是业务流程异步化还是数据库事务异步化,都面临一个如何保证业务事务一致性的问题。面对这个问题目前还没有完美的解决方案。 关于数据库事务,核心是提现数据库...
  • 分布式事务解决方案

    千次阅读 2018-10-12 19:35:33
    2.分布式事务场景 1. 跨库事务 2. 分库分表 3.服务化 3.两阶段提交 &amp; 三阶段提交 &amp; paxos CAP + BASE 4. 典型的柔性事务方案 5. 场景的分布式事务解决方案 1. 基于XA协议的两阶段提交 2. ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 55,722
精华内容 22,288
关键字:

对外事务是做什么的