乐趣区

关于分布式事务:分布式事务一致性业界方案及实践经验总结

综述:日常需要常常须要用到分布式事务一致性保障,踩坑摸索中积攒了一些教训,联合各种内外部材料,对立做一次梳理,并在最初附上一些实践经验

举例一个业务场景:银行账户 A 转账给银行账户 B 100 元
引入问题和要求:

  • A-100 和 B+100 都胜利,或都失败
  • 转账前 A+B 总额 == 转账后 A+B 总额
  • 同一段时间多笔转账间尽量可能并行且不相互影响
  • 转账实现后数据永恒保留不失落

本地(单机)事务场景

如果咱们的业务零碎不简单,能够在一个数据库、一个服务内对数据进行批改,实现转账,那么,咱们能够利用数据库事务,保障转账业务的正确实现。现有的关系型数据库在这方面曾经十分欠缺了

在单机数据库场景下,事务保障 ACID 要求

摘自维基百科:
原子性(Atomicity):一个事务(transaction)中的所有操作,或者全副实现,或者全副不实现,不会完结在两头某个环节。事务在执行过程中产生谬误,会被回滚(Rollback)到事务开始前的状态,就像这个事务素来没有执行过一样。即,事务不可分割、不可约简。[1]
一致性(Consistency):在事务开始之前和事务完结当前,数据库的完整性没有被毁坏。这示意写入的材料必须完全符合所有的预设束缚、触发器、级联回滚等。[1]
事务隔离(Isolation):数据库容许多个并发事务同时对其数据进行读写和批改的能力,隔离性能够避免多个事务并发执行时因为穿插执行而导致数据的不统一。事务隔离分为不同级别,包含未提交读(Read uncommitted)、提交读(read committed)、可反复读(repeatable read)和串行化(Serializable)。[1]
持久性(Durability):事务处理完结后,对数据的批改就是永恒的,即使系统故障也不会失落。[1]

在常见互联网业务场景下,都是读多写少,所以比拟多都是用 MVCC 技术,用版本号和快照的计划实现高并发(repeatable read)下的一致性

分布式事务

现实情况下,绝大多数的互联网业务都是分布式系统,服务、资源、数据库等都不是单机部署,甚至可能横跨多家公司、运营商。所以在这种场景下,原有的本地事务保障曾经无奈满足需要

数据库外部分布式事务

一些分布式数据库原生外部反对事务个性,能够大大减少业务方操作事务的复杂性,根本由数据库组件来解决问题
目前常见的反对分布式事务的数据库举例:

  • TDSQL(腾讯)
  • TBase(腾讯)
  • TiDB(PingCAP)
  • Spanner(Google)
  • OceanBase(阿里巴巴)

这种计划比拟适宜数据位于同一个数据库组件,只须要可能保障并发操作不会造成数据不统一的业务

更宽泛场景下的的异构分布式事务

更多时候,咱们的业务流程都比较复杂,比方上网买个女朋友。外围的流程至多有:

  1. 下单
  2. 领取
  3. 扣库存
  4. 发货

每个步骤可能都是由不同服务操作,相干的数据存储在不同数据库中。这种状况下,难以有对立的版本号,MVCC 伎俩无奈应用
目前为止还没有反对跨数据库的严格一致性计划

绝对应于本地事务的强 ACID 要求,分布式事务场景下,为了面向高可用、可扩大等要求,个别会进行取舍,升高局部一致性和隔离性的要求,遵循 BASE 实践:

  • 根本业务可用(Basic Availability)
  • 软状态(Soft state)
  • 最终统一(Eventual consistency)

分布式事务中的 ACID 状况:

  • 原子性:严格遵循,采纳相似 UNDO 的形式实现
  • 一致性:实现后的一致性严格遵循;事务中的一致性可适当放宽
  • 隔离性:大量事务能够并行
  • 持久性:严格遵循

目前业界常见的分布式事务解决方案有

  • XA(两阶段提交、三阶段提交)
  • TCC
  • SAGA
  • 本地音讯表
  • 事务音讯
  • 最大致力告诉
  • AT 事务模式

XA

XA 是由 X/Open 组织提出的分布式事务标准,这个标准次要定义了(全局)事务管理器(TM)和(部分)资源管理器(RM)之间的接口。本地数据库如 MySQL 对应的是这里的 RM 角色
XA 由一个或多个资源管理器(RM),一个事务管理器(TM)和一个应用程序(ApplicationProgram)组成。这三个角色概念 RM、TM、AP 是经典的角色划分,须要见名知意

目前支流的数据库根本都反对 XA 事务

两阶段提交

顾名思义就是须要分两步提交事务:
第一阶段(prepare):事务管理器向所有本地资源管理器发动申请,询问是否是 ready 状态,所有参与者都将本事务是否胜利的信息反馈发给协调者
第二阶段(commit/rollback):事务管理器依据所有本地资源管理器的反馈,告诉所有本地资源管理器,各自为政地在所有分支上提交或者回滚

长处:

  • 简略容易了解,开发较容易

毛病:

  • 同步阻塞问题:prepare 阶段锁住了资源,其余参与者须要等前一个参与者开释,并发度低
  • 单点问题:事务管理器呈现故障,整个零碎不可用
  • 不统一的可能:commit/rollback 阶段,如果事务管理器只发送了局部 commit 音讯,此时如果事务管理宕机(极其状况某些参与者也一起宕机),则难以保障所有参与者一致性失常

不统一的可能,能够引入 超时机制 互询机制 来很大水平解决:
对于协调者来说如果在指定工夫内没有收到所有参与者的应答,则能够主动退出 WAIT 状态,并向所有参与者发送 rollback 告诉。对于参与者来说如果位于 READY 状态,然而在指定工夫内没有收到协调者的第二阶段告诉,则不能果断地执行 rollback 操作,因为协调者可能发送的是 commit 告诉,这个时候执行 rollback 就会导致数据不统一。
此时,咱们能够染指互询机制,让参与者 A 去询问其余参与者 B 的执行状况。如果 B 执行了 rollback 或 commit 操作,则 A 能够大胆的与 B 执行雷同的操作;如果 B 此时还没有达到 READY 状态,则能够推断出协调者收回的必定是 rollback 告诉;如果 B 同样位于 READY 状态,则 A 能够持续询问另外的参与者。只有当所有的参与者都位于 READY 状态时,此时两阶段提交协定无奈解决,将陷入长时间的阻塞状态。

三阶段提交

针对两阶段提交存在的问题,三阶段提交协定通过引入一个 预询 阶段,以及超时策略来缩小整个集群的阻塞工夫,晋升零碎性能。三阶段提交的三个阶段别离为:预询(can_commit)、预提交(pre_commit),提交(do_commit)

第一阶段:该阶段协调者会去询问各个参与者是否可能失常执行事务,参与者依据本身状况回复一个预估值,绝对于真正的执行事务,这个过程是轻量的,具体步骤如下

1. 协调者向各个参与者发送事务询问告诉,询问是否能够执行事务操作,并期待回复
2. 各个参与者根据本身情况回复一个预估值,如果预估本人可能失常执行事务就返回确定信息,并进入准备状态,否则返回否定信息

第二阶段:本阶段协调者会依据第一阶段的询问后果采取相应操作,询问后果次要有 3 种:

  1. 所有的参与者都返回确定信息
  2. 一个或多个参与者返回否定信息
  3. 协调者期待超时
    针对第一种状况,协调者会向所有参与者发送事务执行申请:
  4. 协调者向所有的事务参与者发送事务执行告诉
  5. 参与者收到告诉后执行事务但不提交
  6. 参与者将事务执行状况返回给客户端

在上述步骤中如果参与者期待超时,以及询问后果的后两种异常情况,则会中断事务,向各个参与者发送 abort 告诉,申请退出准备状态

第三阶段:
如果第二阶段事务未中断,那么本阶段协调者将会根据事务执行返回的后果来决定提交或回滚事务,分为 3 种状况:

  1. 所有的参与者都能失常执行事务
  2. 一个或多个参与者执行事务失败
  3. 协调者期待超时
    针对第 1 种状况,协调者向各个参与者发动事务提交申请,具体步骤如下
  4. 协调者向所有参与者发送事务 commit 告诉
  5. 所有参与者在收到告诉之后执行 commit 操作,并开释占有的资源
  6. 参与者向协调者反馈事务提交后果
    针对第 2 和第 3 种状况,协调者认为事务无奈胜利执行,于是向各个参与者发送事务回滚申请,具体步骤如下
  7. 协调者向所有参与者发送事务 rollback 告诉
  8. 所有参与者在收到告诉之后执行 rollback 操作,并开释占有的资源
  9. 参与者向协调者反馈事务回滚后果

总结:三阶段提交是两阶段提交的进化计划。但在两阶段提交中,长时间资源阻塞和数据不统一产生的可能性还是比拟低的,所以尽管三阶段提交协定绝对于两阶段提交协定对于数据强一致性更有保障,但因过于简单,且效率绝对低,两阶段提交在理论中利用更多

TCC

对于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。TCC 事务机制相比于下面介绍的 XA,解决了其几个毛病:

  1. 同步阻塞:引入超时,超时后进行弥补,并且不会锁定整个资源,将资源转换为业务逻辑模式,粒度变小
  2. 解决了协调者单点,由主业务方发动并实现这个业务流动。业务流动管理器也变成多点,引入集群
  3. 数据一致性,有了弥补机制之后,由业务流动管理器管制一致性

TCC 分为三个阶段:

  • Try 阶段:尝试执行,实现所有业务查看(一致性),预留必须业务资源(准隔离性)
  • Confirm 阶段:确认执行真正执行业务,不做任何业务查看,只应用 Try 阶段预留的业务资源。Confirm 操作要求满足幂等性,失败后要重试
  • Cancel 阶段:勾销执行,开释 Try 阶段预留资源。Cancel 操作也须要幂等性,失败要重试

长处:

  • 并发度高,无需长期锁定资源
  • 一致性较好
  • 实用于对中间状态有束缚的业务

毛病:

  • 开发简单,须要提供 Try、Confirm、Cancel 接口

本地音讯表

本地音讯表这个计划最后是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。设计外围是将须要分布式解决的工作通过音讯的形式来异步确保执行
大抵流程:生产者服务收到订单,则在 DB 写入该订单,并在同一个事务写一条订单音讯到音讯表;存在一个定时器轮询音讯表,发送未发送的音讯到 MQ,上游消费者生产后进行上游业务操作,实现后更新订单状态

容错机制:

  • 扣减余额事务 失败时,事务间接回滚,无后续步骤
  • 轮序生产音讯失败,减少余额事务失败都会进行重试

长处:

  • 长事务仅须要分拆成多个工作,应用简略

毛病:

  • 生产者须要额定的创立音讯表
  • 须要定时器轮询
  • 消费者的逻辑如果无奈通过重试胜利,那么还须要更多的机制,来回滚操作

实用于可异步执行的业务,且后续操作无需回滚的业务

事务音讯

在上述的本地音讯表计划中,生产者须要额定创立音讯表,还须要对本地音讯表进行轮询,业务累赘较重。
阿里开源的 RocketMQ 4.3 之后的版本正式反对事务音讯,该事务音讯实质上是把本地音讯表放到 RocketMQ 上,解决生产端的音讯发送与本地事务执行的原子性问题。

留神 Kafka 和 Pulsar 的事务音讯和 RocketMQ 的事务音讯并不是同一个概念
能够参考:浅谈 RocketMQ、Kafka、Pulsar 的事务音讯 – DockOne.io

事务音讯发送及提交:

  • 发送音讯(half 音讯)
  • 服务端存储音讯,并响应音讯的写入后果
  • 依据发送后果执行本地事务(如果写入失败,此时 half 音讯对业务不可见,本地逻辑不执行)
  • 依据本地事务状态执行 Commit 或者 Rollback(Commit 操作公布音讯,音讯对消费者可见)

容错机制:
对没有 Commit/Rollback 的事务音讯(pending 状态的音讯),从服务端发动一次“回查”
Producer 收到回查音讯,返回音讯对应的本地事务的状态,为 Commit 或者 Rollback
事务音讯计划与本地音讯表机制十分相似,区别次要在于原先相干的本地表操作替换成了一个反查接口

长处:

  • 长事务仅须要分拆成多个工作,并提供一个反查接口,应用简略

毛病:

  • 消费者的逻辑如果无奈通过重试胜利,那么还须要更多的机制,来回滚操作

实用于可异步执行的业务,且后续操作无需回滚的业务

最大致力告诉

最大致力告诉是最简略的一种柔性事务,实用于一些最终一致性工夫敏感度低的业务,且被动方处理结果 不影响被动方的处理结果
大抵意思:

  1. 零碎 A 本地事务执行完之后,发送个音讯到 MQ
  2. 这里会有个专门生产 MQ 的服务,这个服务会生产 MQ 并调用零碎 B 的接口
  3. 要是零碎 B 执行胜利就 ok 了;要是零碎 B 执行失败了,那么最大致力告诉服务就定时尝试从新调用零碎 B, 重复 N 次,最初还是不行就放弃

与后面本地音讯表和事务音讯计划比照,区别:
可靠消息一致性,发动告诉方须要保障将音讯收回去,并且将音讯发到接管告诉方,音讯的可靠性要害由发动告诉方来保障。
最大致力告诉,发动告诉方尽最大的致力将业务处理结果告诉为接管告诉方,然而可能音讯接管不到,此时须要接管告诉方被动调用发动告诉方的接口查问业务处理结果,告诉的可靠性要害在接管告诉方

解决方案上,最大致力告诉须要:

  • 提供接口,让承受告诉放可能通过接口查问业务处理结果
  • 音讯队列 ACK 机制,音讯队列依照距离 1min、5min、10min、30min、1h、2h、5h、10h 的形式,逐渐拉大告诉距离,直到达到告诉要求的工夫窗口下限。之后不再告诉

最大致力告诉实用于业务告诉类型,例如微信交易的后果,就是通过最大致力告诉形式告诉各个商户,既有回调告诉,也有交易查问接口

SAGA

Saga 是 1987 年普林斯顿大学的 Hector Garcia-Molina 和 Kenneth Salem 发表的数据库论文 Sagas 提到的一个计划。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果失常完结那就失常实现,如果某个步骤失败,则依据相同程序顺次调用弥补操作

Saga 一旦到了 Cancel 阶段,那么 Cancel 在业务逻辑上是不容许失败了。如果因为网络或者其余长期故障,导致没有返回胜利,那么 TM 会一直重试,直到 Cancel 返回胜利

论文外面的 SAGA 内容较多,包含两种复原策略,包含分支事务并发执行等等,能够参考 Managing data consistency in a microservice architecture using Sagas – part 1 来学习

复原策略:

  1. 向后复原:如果任一子事务失败,弥补所有已实现的事务
  2. 向前复原:重试失败的事务,假如每个子事务最终都会胜利(这种状况不须要提供弥补能力)

留神:Saga 并不反对 ACID 的 I 也就是隔离性,须要业务层面自行解决
应答论文计划隔离性的缺失,Seata 用到了状态机(Seata Saga 模式)

Saga 是一种“长事务的解决方案”,更适宜于“业务流程长、业务流程多”的场景
应用场景举例:用户在某平台下一个关联订单,蕴含机票预订、租车、酒店预订、领取等流程,如果所有动作都胜利,则事务实现,否则所有流程要弥补(并不一定是回滚,比方机票、租车的子订单状态变为勾销预订而不是删除订单)

长处:

  • 一阶段提交本地事务,无锁,高性能
  • 参与者可异步执行,高吞吐
  • 弥补服务易于实现,因为一个更新操作的反向操作是比拟容易了解的

毛病:

  • 原始计划不反对隔离性

AT 事务模式

这是阿里开源我的项目 seata 中的一种事务模式,在蚂蚁金服也被称为 FMT。长处是该事务模式应用形式,相似 XA 模式,业务无需编写各类弥补操作,回滚由框架主动实现,毛病也相似 XA,存在较长时间的锁,不满足高并发的场景。从性能的角度看,AT 模式会比 XA 更高一些,但也带来了脏回滚这样的新问题。有趣味的同学能够参考 seata-AT

其余

微信事件核心 – 高牢靠、高可用的事务音讯平台
分两种模式

  1. 一般音讯,实质是把所有相干事务都异步化执行,依赖音讯队列驱动的有限重试机制,保障最终全副胜利
  2. 基于事件核心实现的两阶段提交,看上去是上文提到的 事务音讯 + 两阶段提交 的组合模式,具体设计原由及选用场景见文章即可

这里看架构图集体了解划分这两种模式,是因为微信业务接入的第一步和前文所述的本地音讯表 && 事件音讯模式不太一样造成的。微信这里的模式是,业务要执行事务(无论主事务还是从事务),第一步都是向事件核心提交申请;而前者的操作是主事务执行胜利后才会生成音讯触发从事务

异样解决及一些开发实际

分布式事务组件会遇到的一些经典问题:

  • 空回滚(比方 TCC 中 Cancel 了一个没有 Try 的事务)
  • 申请反复(这种比拟常见,要解决订单号幂等)
  • 悬挂(比方 TCC 中因时序问题,导致 Cancel 比 Try 先收到)
    解决办法能够参考开源组件 DTM 的子事务屏障 | DTM 教程机制

开发角度:
既然是事务,无论是否分布式,在业务层面都要留神有一个从头至尾的惟一事务标识,比方订单号。且各个业务接口实现都要留神幂等和可重入逻辑

理解应用的是强统一还是最终统一,明确最终统一的计划对用户的影响,是否须要给用户一些敌对提醒

能够发现,分布式事务的牢靠实现,基本一点就是 谬误重试,但在写重试逻辑的时候,要十分留神,不要过分暴力,在零碎异样的时候,很可能暴力的重试会加剧零碎异样,甚至引发雪崩。比拟常见的办法是管制重试次数(RPC 重试最多不要超过 3 次,音讯重放次数不要超过 50 次,异样音讯最恶化到死信队列解决),或管制重试的时延(比方 1s、2s、4s、8s…)

如果没有牢靠的分布式事务组件能够间接应用(如 Seata、DTM),而是须要在日常开发中自行实现,最简略的计划(但不牢靠,属于仅仅能跑的玩具领域)就是基于现有音讯队列,实现音讯驱动的异步重试事务执行逻辑。即生成一条事务形容音讯(蕴含全副子事务形容)写进音讯队列,上游消费者生产音讯,而后顺次执行每个子事务,任何一个子事务执行失败都 NACK,期待下次重试。但这种计划要留神几个点:

  • 音讯入队失败的解决
  • 不可回滚
  • 每个子事务的执行都要求业务接口幂等
  • 总体事务执行结束工夫无奈保障,最终一致性
  • 有可能呈现永远无奈重试胜利的音讯,须要有监测和解决伎俩(死信队列、监控告警等)
  • 最好可能可能额定保留一份音讯数据,避免音讯队列组件有问题时音讯失落造成不可挽回的影响
  • 留神业务是否有时效性要求(有可能音讯重试屡次后业务层面超时有效了)

无论对本人的架构设计如许自信,对账环节都必不可少,在一开始设计业务流程和数据结构的时候,就要提前思考如何形象对立,不便后续对账零碎的开发,以及如何升高新业务接入对账零碎的复杂性,这里还是挺有挑战的

经营平安角度:
留神一个准则,会对用户产生利益的事务,尽量放到最初再执行(比方一个抽奖业务,先抽奖发奖,而后再扣用户奖券或虚构币,如果第二步有失败或提早,会造成问题)。但还要思考另一个因素,比方 A 相比 B 来说业务逻辑简单很多,失败率高,那这种状况要思考优先执行 A。尽量避免先执行 B,后执行 A 失败要回滚 B 的问题

回滚、弥补的思考因素:
在分布式事务场景下,业务和架构设计,尽量可能不回滚(弥补),而是尽力向前(即 Saga 中的向前复原),能够大大加重开发运维复杂度
另外须要留神回滚、弥补的时效性是否可能承受,在未达到最终统一前,零碎的两头态是否会裸露给用户,以及裸露给用户是否在产品层面能承受


援用:

  • ACID – 维基百科,自在的百科全书
  • 浅谈数据库并发管制 – 锁和 MVCC – 面向信奉编程
  • X/Open XA – 维基百科,自在的百科全书
  • 分布式事务:两阶段提交与三阶段提交 – SegmentFault 思否
  • 分布式事务,这一篇就够了 | 小米信息部技术团队
  • 分布式事务最经典的七种解决方案 – SegmentFault 思否
  • 微服务场景下的数据一致性解决方案 – 又拍云
  • GitHub – seata/seata: Seata is an easy-to-use, high-performance, open source distributed transaction solution.
  • GitHub – yedf/dtm: 🔥A cross-language distributed transaction manager. Support xa, tcc, saga, transactional messages. 跨语言分布式事务管理器
  • 微信事件核心 – 高牢靠、高可用的事务音讯平台
退出移动版