分布式事务

截取转载自https://draveness.me/distributed-transaction-principle

事务实现原理

  • 事务日志
    为了实现确保事务能在执行的任意过程中回滚(原子性)并且提交的事务会永久保存在数据库中,我们会使用事务日志来存储事务执行过程中的数据库的变动,每一条事务日志中都包含事务的 ID、当前被修改的元素、变动前以及变动后的值


    image.png
  • 并发控制
    数据库作为最关键的后端服务,很难想象只能串行执行每一个数据库操作带来的性能影响,然而在并发执行 SQL 的过程中就可能无法保证数据库对于隔离性的要求,归根结底这就是一致性、隔离性与性能之间的权衡。
    为了避免并发带来的一致性问题、满足数据库对于隔离性要求,数据库系统往往都会使用并发控制机制尽可能地充分利用机器的效率,最常见的几种并发控制机制就是锁、时间戳和 MVCC。
    作为悲观并发控制机制,锁使用在更新资源之前对资源进行锁定的方式保证多个数据库的会话同时修改某一行记录时不会出现脱离预期的行为,而时间戳这种方式在每次提交时对资源是否被改变进行检查。

分布式事务

从单机的数据库事务变成分布式事务时,原有单机中相对可靠的方法调用以及进程间通信方式已经没有办法使用,同时由于网络通信经常是不稳定的,所以服务之间信息的传递会出现障碍。
当我们通过网络请求其他服务的接口时,往往会得到三种结果:正确、失败和超时,无论是成功还是失败,我们都能得到唯一确定的结果,超时代表请求的发起者不能确定接受者是否成功处理了请求,这也是造成诸多问题的诱因。
系统之间的通信可靠性从单一系统中的可靠变成了微服务架构之间的不可靠,分布式事务其实就是在不可靠的通信下实现事务的特性。无论是事务还是分布式事务实现原子性都无法避免对持久存储的依赖,事务使用磁盘上的日志记录执行的过程以及上下文,这样无论是需要回滚还是补偿都可以通过日志追溯,而分布式事务也会依赖数据库、Zookeeper 或者 ETCD 等服务追踪事务的执行过程,总而言之,各种形式的日志是保证事务几大特性的重要手段。

2PC 与 3PC

  • 2PC
    两阶段提交的执行过程就跟它的名字一样分为两个阶段,投票阶段和提交阶段,在投票阶段中,协调者(Coordinator)会向事务的参与者(Cohort)询问是否可以执行操作的请求,并等待其他参与者的响应,参与者会执行相对应的事务操作并记录重做和回滚日志,所有执行成功的参与者会向协调者发送 AGREEMENT 或者 ABORT 表示执行操作的结果。
    当所有的参与者都返回了确定的结果(同意或者终止)时,两阶段提交就进入了提交阶段,协调者会根据投票阶段的返回情况向所有的参与者发送提交或者回滚的指令。
    当事务的所有参与者都决定提交事务时,协调者会向参与者发送 COMMIT 请求,参与者在完成操作并释放资源之后向协调者返回完成消息,协调者在收到所有参与者的完成消息时会结束整个事务;与之相反,当有参与者决定 ABORT 当前事务时,协调者会向事务的参与者发送回滚请求,参与者会根据之前执行操作时的回滚日志对操作进行回滚并向协调者发送完成的消息,在提交阶段,无论当前事务被提交还是回滚,所有的资源都会被释放并且事务也一定会结束。


    image.png

    image.png

    image.png

    image.png
  • 3PC
    为了解决两阶段提交在协议的一些问题,三阶段提交引入了超时机制和准备阶段,如果协调者或者参与者在规定的之间内没有接受到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,准备阶段的引入其实让事务的参与者有了除回滚之外的其他选择。


    image.png

    当参与者向协调者发送 ACK 后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,不会像两阶段提交中被阻塞住;上述的图片非常清楚地说明了在不同阶段,协调者或者参与者的超时会造成什么样的行为。

Saga

两阶段提交其实可以保证事务的强一致性,但是在很多业务场景下,我们其实只需要保证业务的最终一致性,在一定的时间窗口内,多个系统中的数据不一致是可以接受的,在过了时间窗口之后,所有系统都会返回一致的结果。

Saga 其实就一种简化的分布式事务解决方案,它将一系列的分布式操作转化成了一系列的本地事务,在每一个本地事务中我们都会更新数据库并且向集群中的其他服务发送一条的新的消息来触发下一个本地的事务;一旦本地的事务因为违反了业务逻辑而失败,那么就会立刻触发一系列的回滚操作来撤回之前本地事务造成的副作用。
Saga 这种模式其实完全放弃了同时满足事务四大基本特性 ACID 的想法,而是选择降低实现分布式事务的难度并减少资源同步以及锁定带来的问题,选择实现 BASE(Basic Availability, Soft, Eventual consistency) 事务,达到业务上的基本可用以及最终一致性,在绝大多数的业务场景中,实现最终一致性就能够基本满足业务的全部需求,极端场景下还是应该选择两阶段提交或者干脆放弃分布式事务这种易错的实现方式,转而使用单机中的数据库事务来解决。

消息服务

分布式事务带来复杂度的原因其实就是由于各个模块之间的通信不稳定,当我们发出一个网络请求时,可能的返回结果是成功、失败或者超时。
网络无论是返回成功还是失败其实都是一个确定的结果,当网络请求超时的时候其实非常不好处理,在这时调用方并不能确定这一次请求是否送达而且不会知道请求的结果,但是消息服务可以保证某条信息一定会送达到调用方;大多数消息服务都会提供两种不同的 QoS,也就是服务的等级。
最常见的两种服务等级就是 At-Most-Once 和 At-Least-Once,前者能够保证发送方不对接收方是否能收到消息作保证,消息要么会被投递一次,要么不会被投递,这其实跟一次普通的网络请求没有太多的区别;At-Least-Once 能够解决消息投递失败的问题,它要求发送者检查投递的结果,并在失败或者超时时重新对消息进行投递,发送者会持续对消息进行推送,直到接受者确认消息已经被收到,相比于 At-Most-Once,At-Least-Once 因为能够确保消息的投递会被更多人使用。

除了这两种常见的服务等级之外,还有另一种服务等级,也就是 Exactly-Once,这种服务等级不仅对发送者提出了要求,还对消费者提出了要求,它需要接受者对接收到的所有消息进行去重,发送者和接受者一方对消息进行重试,另一方对消息进行去重,两者分别部署在不同的节点上,这样对于各个节点上的服务来说,它们之间的通信就是 Exactly-Once 的,但是需要注意的是,Exacly-Once 一定需要接收方的参与。

我们可以通过实现 AMQP 协议的消息队列来实现分布式事务,在协议的标准中定义了 tx_select、tx_commit 和 tx_rollback 三个事务相关的接口,其中 tx_select 能够开启事务,tx_commit 和 tx_rollback 分别能够提交或者回滚事务。

使用消息服务实现分布式事务在底层的原理上与其他的方法没有太多的差别,只是消息服务能够帮助我们实现的消息的持久化以及重试等功能,能够为我们提供一个比较合理的 API 接口,方便开发者使用。

你可能感兴趣的:(分布式事务)