关于分布式:基于Seata探寻分布式事务的实现方案

28次阅读

共计 7983 个字符,预计需要花费 20 分钟才能阅读完成。

作者:京东物流技术与数据智能部 张硕

1 背景常识

随着业务的疾速倒退、业务复杂度越来越高,简直每个公司的零碎都会从单体走向分布式,特地是转向微服务架构。随之而来就必然遇到分布式事务这个难题,这篇文章通过 seata 框架总结了分布式事务的几种解决方案

1.1 ACID

关系型数据库具备解决简单事务场景的能力,关系型数据库的事务满足 ACID 的个性。

  • Atomicity:原子性(要么都做,要么都不做)
  • Consistency:一致性(数据库只有一个状态,不存在未确定状态)
  • Isolation:隔离性(事务之间互不烦扰)
  • Durability:永久性(事务一旦提交,数据库记录永恒不变)

1.2 CAP

CAP 是指在一个分布式系统下,蕴含三个因素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),并且三者不可得兼。

  • C:Consistency,一致性,所有数据变动都是同步的。
  • A:Availability,可用性,即在能够承受的工夫范畴内正确地响应用户申请。
  • P:Partition tolerance,分区容错性,即某节点或网络分区故障时,零碎仍可能提供满足一致性和可用性的服务。

1.3 BASE

BASE 实践次要是解决 CAP 实践中分布式系统的可用性和一致性不可兼得的问题。BASE 实践蕴含以下三个因素:

  • BA:Basically Available,根本可用。
  • S:Soft State,软状态,状态能够有一段时间不同步。
  • E:Eventually Consistent,最终统一,最终数据是统一的就能够了,而不是时时放弃强统一。

2 实现模式

2.1 二段提交

第一阶段(筹备阶段)

TM 告诉所有参加事务的各个 RM,给每个 RM 发送 prepare 音讯。
RM 接管到音讯后进入筹备阶段后,要么间接返回失败,要么创立并执行本地事务,写本地事务日志(redo 和 undo 日志),然而不提交(此处只保留最初一步耗时起码的提交操作给第二阶段执行)。

第二阶段(提交 / 回滚阶段)

Seata 框架

基于两阶段提交模式,从设计上咱们能够将整体分成三个大模块,即 TM、RM、TC,具体解释如下:

  • TM(Transaction Manager):全局事务管理器,管制全局事务边界,负责全局事务开启、全局提交、全局回滚。
  • RM(Resource Manager):资源管理器,管制分支事务,负责分支注册、状态汇报,并接管事务协调器的指令,驱动分支(本地)事务的提交和回滚。
  • TC(Transaction Coordinator):事务协调器,保护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

一个典型的分布式事务过程:

  • TM 向 TC 申请开启一个全局事务,全局事务创立胜利并生成一个全局惟一的 XID。
  • XID 在微服务调用链路的上下文中流传。
  • RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  • TM 向 TC 发动针对 XID 的全局提交或回滚决定。
  • TC 调度 XID 下管辖的全副分支事务实现提交或回滚申请。

[]()2.2 XA

在 XA 模式下,每一个 XA 事务都是一个事务参与者。分布式事务开启之后

首先在一阶段执行“xa start”、“业务 SQL”、“xa end”和“xa prepare”实现 XA 事务的执行和预提交;

二阶段如果提交的话就执行“xa commit”,如果是回滚则执行“xa rollback”。这样便能保障所有 XA 事务都提交或者都回滚。

无论 Phase2 的决定是 commit 还是 rollback,事务性资源的锁都要放弃到 Phase2 实现才开释。

一个失常运行的业务,大概率是 90% 以上的事务最终应该是胜利提交的,咱们是否能够在 Phase1 就将本地事务提交呢?这样 90% 以上的状况下,能够省去 Phase2 持锁的工夫,整体提高效率。

分支事务中数据的本地锁由本地事务管理,在分支事务 Phase1 完结时开释,这时候其余本地事务就能读取到最新的数据。– 同时,随着本地事务完结,连贯也得以开释。– 分支事务中数据的全局锁在事务协调器治理,在决定 Phase2 全局提交时,全局锁马上能够开释,留神这里是先开释锁,再进行分支事务的提交过程。只有在决定全局回滚的状况下,全局锁才被持有至分支的 Phase2 完结,即所有分支事务回滚完结。

这个设计,极大地缩小了分支事务对资源(数据和连贯)的锁定工夫,给整体并发和吞吐的晋升提供了根底。然而分布式事务的隔离级别变动了

XA 模式和 上面的 AT 模式一样是一种对业务无侵入性的解决方案;但与 AT 模式不同的是,XA 模式将快照数据和行锁等通过 XA 指令委托给了数据库来实现,这样 XA 模式实现更加轻量化

2.3 AT

AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注本人的“业务 SQL”,用户的“业务 SQL”就是全局事务一阶段,Seata 框架会主动生成事务的二阶段提交和回滚操作。

一阶段

首先,利用要应用 Seata 的 JDBC 数据源代理,也就是后面提到的 RM 概念,所有对 DB 的操作都是通过 Seata RM 代理实现。在这层代理中,Seata 会自动控制 SQL 的执行,提交,回滚。

Seata 代理会把业务数据在更新前后的数据镜像 (beforeImage & afterImage) 组织成回滚日志,利用本地事务的 ACID 个性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。这样,能够保障:任何提交的业务数据的更新肯定有相应的回滚日志存在。

而后,本地事务在提交之前, 还须要通过 RM 向 TC 注册本地分支,这个注册过程中会依据方才执行的 SQL 拿到所有波及到的数据主键,以 resourceId + tableName + rowPK 作为锁的 key,向 TC 申请所有波及数据的写锁,当取得所有相干数据的写锁后,再执行本地事务的 Commit 过程。如果有任何一行数据的写锁没有拿到的话,TC 会以 fastfail 的形式回复该 RM,RM 会以重试 + 超时机制反复该过程,直到超时。

实现本地事务后,RM 会向 TC 汇报本地事务的执行状况,并实现业务 RPC 的调用过程。

二阶段

case1: 如果 TM 决定是全局提交,此时分支事务实际上曾经实现提交,TC 立即开释该全局事务的所有锁,而后异步调用 RM 清理回滚日志,Phase2 能够十分疾速地实现。

case2: 如果决定是全局回滚,RM 收到协调器发来的回滚申请,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以实现分支的回滚。当分支回滚顺利完结时,告诉 TC 回滚实现,这时候 TC 才开释该分支事务相干的所有锁。

注:RM 在进行回滚时,会先跟 afterImage 进行比拟:– 如果统一:则执行逆向 SQL – 如果不统一:再跟 beforeImage 进行比拟 – 如果统一:阐明没必要执行回滚 SQL 了,数据曾经复原了 – 如果不统一:阐明呈现了脏数据,这时候就抛出异样,须要人工解决

2.4 TCC

TCC 模式须要用户依据本人的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方先在 TC 中注册全局事务,而后在一阶段执行 Try 办法,在二阶段提交的话 TC 会去执行各个 RM 的 Confirm 办法,二阶段回滚则 TC 会去执行各个 RM 的 Cancel 办法。

与 AT 模式一样,Seata 会给理论办法的执行加切面,该切面会拦挡所有对 TCC 接口的调用。在调用 Try 接口时,如果发现处在全局事务中,切面会先向 TC 注册一个分支事务,和 AT 不同的是 TCC 注册分支事务是不加锁的,注册实现后去执行原来的 RPC 调用。当申请链路调用实现后,TC 通过分支事务的资源 ID 回调到正确的参与者去执行对应 TCC 资源的 Confirm 或 Cancel 办法。

TCC 模式的整体框架绝对于 AT 来说更加简略,次要是扫描 TCC 接口,注册资源,拦挡接口调用,注册分支事务,最初回调二阶段接口。最外围的实际上是 TCC 接口的实现逻辑。

1)应用准则

从 TCC 模型的框架能够发现,TCC 模型的外围在于 TCC 接口的设计。用户在接入 TCC 时,大部分工作都集中在如何实现 TCC 服务上。这就是 TCC 模式最次要的问题,对业务侵入比拟大,要花很大的功夫来实现 TCC 服务。

设计一套 TCC 接口最重要的是什么?次要有两点,第一点,须要将操作分成两阶段实现。TCC(Try-Confirm-Cancel)分布式事务模型绝对于 XA 等传统模型,其特色在于它不依赖 RM 对分布式事务的反对,而是通过对业务逻辑的合成来实现分布式事务。

TCC 分布式事务模型须要业务零碎提供三段业务逻辑:1. 初步操作 Try:实现所有业务查看,预留必须的业务资源。2. 确认操作 Confirm:真正执行的业务逻辑,不做任何业务查看,只应用 Try 阶段预留的业务资源。因而,只有 Try 操作胜利,Confirm 必须能胜利。另外,Confirm 操作需满足幂等性,保障一笔分布式事务能且只能胜利一次。3. 勾销操作 Cancel:开释 Try 阶段预留的业务资源。同样的,Cancel 操作也须要满足幂等性。因而,TCC 模型的隔离性思维就是通过业务的革新,在第一阶段完结之后,从底层数据库资源层面的加锁过渡为下层业务层面的加锁,开释底层数据库锁资源,放宽分布式事务锁协定,将锁的粒度降到最低,以最大限度进步业务并发性能。

第二点,就是要依据本身的业务模型去管制并发,Seata 框架自身仅提供两阶段原子提交协定,保障分布式事务原子性。事务的隔离须要交给业务逻辑来实现。隔离的实质就是管制并发,避免并发事务操作雷同资源而引起的后果错乱。例如:“账户 A 上有 100 元,事务 T1 要扣除其中的 30 元,事务 T2 也要扣除 30 元,呈现并发”。在第一阶段 Try 操作中,须要先利用数据库资源层面的加锁,查看账户可用余额,如果余额短缺,则预留业务资源加到各自的解冻里,扣除本次交易金额,一阶段完结后,尽管数据库层面资源锁被开释了,但这笔资金被业务隔离,不容许除本事务之外的其它并发事务动用。

2)异样管制

空回滚

空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 办法的状况下,调用了二阶段的 Cancel 办法,Cancel 办法须要辨认出这是一个空回滚,而后间接返回胜利。

Cancel 要辨认出空回滚,间接返回胜利。那要害就是要辨认出这个空回滚。思路很简略就是须要晓得一阶段是否执行,如果执行了,那就是失常回滚;如果没执行,那就是空回滚。因而,须要一张额定的事务管制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 办法里会插入一条记录,示意一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则失常回滚;如果该记录不存在,则是空回滚。

悬挂

悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为容许空回滚的起因,Cancel 接口认为 Try 接口没执行,空回滚间接返回胜利,对于 Seata 框架来说,认为分布式事务的二阶段接口曾经执行胜利,整个分布式事务就完结了。然而这之后 Try 办法才真正开始执行,预留业务资源,回忆一下后面提到事务并发管制的业务加锁,对于一个 Try 办法预留的业务资源,只有该分布式事务能力应用,然而 Seata 框架认为该分布式事务曾经完结,也就是说,当呈现这种状况时,该分布式事务第一阶段预留的业务资源就再也没有人可能解决了。

比方在 RPC 调用时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络产生拥挤,通常 RPC 调用是有超时工夫的,RPC 超时当前,发起方就会告诉 TC 回滚该分布式事务,可能回滚实现后,RPC 申请才达到参与者,真正执行,从而造成悬挂。

幂等

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

解决思路

Try 办法次要须要思考两个问题,一个是 Try 办法须要可能通知二阶段接口,曾经预留业务资源胜利。第二个是须要查看第二阶段是否曾经执行实现,如果已实现,则不再执行

Confirm 办法。因为 Confirm 办法不容许空回滚,也就是说,Confirm 办法肯定要在 Try 办法之后执行。因而,Confirm 办法只须要关注反复提交的问题。须要一张事务执行记录表,能够先锁定事务记录,如果事务记录为空,则阐明是一个空提交,不容许,终止执行。如果事务记录不为空,则持续查看状态是否为初始化,如果是,则阐明一阶段正确执行,那二阶段失常执行即可。如果状态是已提交,则认为是反复提交,间接返回胜利即可;如果状态是已回滚,也是一个异样,一个已回滚的事务,不能从新提交,须要可能拦挡到这种异常情况,并报警。

Cancel 办法。因为 Cancel 办法容许空回滚,并且要在先执行的状况下,让 Try 办法感知到 Cancel 曾经执行,所以和 Confirm 办法略有不同。首先仍然是锁定事务记录。如果事务记录为空,则认为 Try 办法还没执行,即是空回滚。空回滚的状况下,应该先插入一条事务记录,确保后续的 Try 办法不会再执行。如果插入胜利,则阐明 Try 办法还没有执行,空回滚继续执行。如果插入失败,则认为 Try 办法正在执行,期待 TC 的重试即可。如果一开始读取事务记录不为空,则阐明 Try 办法曾经执行结束,再查看状态是否为初始化,如果是,则还没有执行过其余二阶段办法,失常执行 Cancel 逻辑。如果状态为已回滚,则阐明这是反复调用,容许幂等,间接返回胜利即可。如果状态为已提交,则同样是一个异样,一个已提交的事务,不能再次回滚

2.5 Saga

Saga 模式是 Seata 行将开源的长事务解决方案。在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正弥补服务,须要用户依据业务场景实现其正向操作和逆向回滚操作。

分布式事务执行过程中,顺次执行各参与者的正向操作,如果所有正向操作均执行胜利,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行后面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

Saga 正向服务与弥补服务也须要业务开发者实现。有点像是 TCC 模式将 Try 过程和 Confirm 过程合并,所有参与者间接执行 Try + Confirm,如果有人失败了,就反向顺次 Cancel。

因为该模式次要用于长事务场景,所以通常是由事件驱动的,各个参与者之间是异步执行的。

Saga 模式实用于业务流程长且须要保障事务最终一致性的业务零碎,Saga 模式一阶段就会提交本地事务,无锁、长流程状况下能够保障性能

Saga 模式的劣势是:

  • 一阶段提交本地数据库事务,无锁,高性能;
  • 参与者能够采纳事务驱动异步执行,高吞吐;
  • 弥补服务即正向服务的“反向”,易于了解,易于实现;

毛病:Saga 模式因为一阶段曾经提交本地数据库事务,且没有进行“预留”动作,所以不能保障隔离性。

事务隔离

纵观 Seata 提供的所有分支事务模式, 除了 AT 模式和 XA 模式能够运行在读已提交的隔离级别下, 其余模式都是运行在读未提交的级别下。在有必要时,利用须要通过业务逻辑的奇妙设定,来解决分布式事务隔离级别带来的问题

AT 模式通过全局写排他锁,来保障事务间的写隔离,将全局事务默认定义在读未提交的隔离级别上,全局事务读未提交,并不是说本地事务的 db 数据没有失常提交,而是指全局事务二阶段 commit | rollback 未真正解决完(即未开释全局锁),而且这时候其余事务会读到一阶段提交的内容。

有些利用如果须要达到全局的读已提交,AT 也提供了相应的机制来达到目标,那就是 select for update + @GlobalLock, 当执行该命令时 RM 会去 TC 确认该锁是否由别人占有, 这样如果有一个分布式事务 T1 正在进行中时, 另一个事务 T2 会因为发现锁抵触而阻塞后续代码的执行, 当后面的分布式事务 T1 完结时, 开释了相应的资源锁, T2 能力读取到相应的数据, 这样就达到读已提交的成果

2.6 音讯组件

利用 MQ 组件实现的二阶段提交。此计划波及 3 个模块:

  • 上游利用,执行业务并发送 MQ 音讯。
  • 可靠消息服务和 MQ 音讯组件,协调上下游音讯的传递,并确保上下游数据的一致性。
  • 上游利用,监听 MQ 的音讯并执行本身业务。

上游利用将本地业务执行和音讯发送绑定在同一个本地事务中,保障要么本地操作胜利并发送 MQ 音讯,要么两步操作都失败并回滚。

  1. 上游利用发送待确认音讯到可靠消息零碎
  2. 可靠消息零碎保留待确认音讯并返回
  3. 上游利用执行本地业务
  4. 上游利用告诉可靠消息零碎确认业务已执行并发送音讯。
  5. 可靠消息零碎批改音讯状态为发送状态并将音讯投递到 MQ 中间件。
  1. 上游利用监听 MQ 音讯组件并获取音讯
  2. 上游利用依据 MQ 音讯体信息处理本地业务
  3. 上游利用向 MQ 组件主动发送 ACK 确认音讯被生产
  4. 上游利用告诉可靠消息零碎音讯被胜利生产,可靠消息将该音讯状态更改为已实现。

异样解决

上游异样
可靠消息服务定时监听音讯的状态,如果存在状态为待确认并且超时的音讯,则示意上游利用和可靠消息交互中的步骤 4 或者 5 出现异常。

  1. 可靠消息查问超时的待确认状态的音讯
  2. 向上游利用查问业务执行的状况
  3. 业务未执行,则删除该音讯,保障业务和可靠消息服务的一致性。业务已执行,则批改音讯状态为已发送,并发送音讯到 MQ 组件。

上游异样

  1. 可靠消息服务定时查问状态为已发送并超时的音讯
  2. 可靠消息将音讯从新投递到 MQ 组件中
  3. 上游利用监听音讯,在满足幂等性的条件下,从新执行业务。
  4. 上游利用告诉可靠消息服务该音讯曾经胜利生产。

理论过程中,还须要引入人工干预性能。比方引入重发次数限度,超过重发次数限度的将音讯批改为死亡音讯,期待人工解决。

3 总结

3.1 sql 反对上

AT 其实就是一个自实现的 XA 事务,其实能够晓得,AT 在 sql 反对上远不迭 XA 模式,AT 须要做 sql 解析背地的实现只能本人解决,目前只能靠社区的贡献者来提供解决方案,这是一个长期的关键性的问题,也有很多用户抉择在 AT 模式上重写 sql 来获取 AT 模式的反对,sql 反对上 XA 是完胜的。

3.2 隔离性

AT 模式是通过解析 sql 获取波及的主键 id,生成行锁。也就是 AT 模式的隔离靠的是全局锁来保障的,粒度细至行级。锁信息存储在 seata server 侧。XA 的隔离级别是由本地数据库保障,锁存储在各个本地数据库中。因为 XA 模式一旦执行了 prepare 后,再也无奈重入这个 XA 事务也无奈跟其余 XA 事务共享锁,因为 XA 协定仅仅是通过 XID 来 start 一个事务,自身不存在分支事务的说法。也就是说他只管本人

3.3 入侵性

通过下面的信息能够发现谁更底层,入侵性则更小,所以由数据库本身反对的 XA 模式来说,入侵性无疑最小,应用老本最低。XA 的 RM 理论是在数据库,而 AT 则是以中间件层部署在利用这一侧的,不依赖数据库自身的协定反对,这点对于微服务架构来说是至关重要的。应用层不须要为本地事务和分布式事务多类不同场景来适配多套不通的驱动

3.4 补偿性事务的问题

实质上 seata 框架反对了 3 大弥补事务模式,AT, TCC,Saga 都是弥补型的。弥补型事务处理机制是构建在事务资源之上的,事务资源自身对分布式事务是无感知的,事务资源对分布式事务无感知存在一个根本性问题,就是无奈做到真正的全局一致性。比方一条库存记录,处在弥补型事务处理过程中,由 100 扣减为 50,此时仓库管理员连贯数据库查看就会查问到 50,之后事务异样回滚,库存就会被弥补回滚为 100,显然仓库管理员查问到的 50 就是脏数据。那 XA 的价值是什么,与弥补型事务不同,XA 协定要求事务资源自身提供对标准和协定的反对。因为事务资源感知并参加分布式事务处理过程,所以事务资源能够保障从任意视角对数据的拜访无效隔离,比方下面 XA 事务处理过程中,两头态的 50 是不会被查问到的(当然隔离级别要在读已提交以上),以此来满足全局一致性。

正文完
 0