分布式事务-TCC

4次阅读

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

全局事务(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 具体的实现是由数据库厂商来完成的。
基于可靠消息服务的分布式事务 这种实现分布式事务的方式需要通过消息中间件来实现。假设有 A 和 B 两个系统,分别可以处理任务 A 和任务 B。此时系统 A 中存在一个业务流程,需要将任务 A 和任务 B 在同一个事务中处理。

系统 A 处理任务 A 前,首先向消息中间件发送一条消息

消息中间件收到后将该条消息持久化,但并不投递。此时下游系统 B 仍然不知道该条消息的存在。

消息中间件持久化成功后,便向系统 A 返回一个确认应答;

系统 A 收到确认应答后,则可以开始处理任务 A;

任务 A 处理完成后,向消息中间件发送 Commit 消息。该请求发送完成后,对系统 A 而言,该事务的处理过程就结束了,此时它可以处理别的任务。

但 Commit 消息可能会在传输途中丢失,从而消息中间件并不会向系统 B 投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。

消息中间件收到 Commit 指令后,便向系统 B 投递该消息,从而触发任务 B 的执行;

当任务 B 执行完成后,系统 B 向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。

上述过程可以得出如下几个结论

消息中间件扮演者分布式事务协调者的角色。

系统 A 完成任务 A 后,到任务 B 执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足 CAP 理论。

如果任务 A 处理失败,那么需要进入回滚流程

    若系统 A 在处理任务 A 时失败,那么就会向消息中间件发送 Rollback 请求。和发送 Commit 请求一样,系统 A 发完之后便可以认为回滚已经完成,它便可以去做其他的事情。

消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统 B,从而不会触发系统 B 的任务 B。

系统又处于一致性状态,因为任务 A 和任务 B 都没有执行。上述 Commit 和 Rollback 都属于理想情况,在实际系统中,Commit 和 Rollback 指令都有可能在传输途中丢失。这种情况就需要超时询问机制。

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

提交,则将该消息投递给系统 B。

回滚,则直接将条消息丢弃。

处理中,则继续等待。

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

    当系统 A 将向消息中间件发送 Commit 指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统 A 事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本,因此在消息投递失败后为什么不回滚消息,而是不断尝试重新投递。

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

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

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

    异步能提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。

我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的 CAP 理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。

最大努力通知(定期校对)

最大努力通知也被称为定期校对,其实在消息中间件方案中已经包含,这种方案也需要消息中间件的参与。

    上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,然后上游系统可以去做别的事情了;

    消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;

    当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。

    上面是一个理想化的过程,但在实际场景中,往往会出现如下几种意外情况:

        消息中间件向下游系统投递消息失败

        上游系统向消息中间件发送消息失败

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

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

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

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

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

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

    Confirm 执行业务,这个过程真正开始执行业务,由于 Try 阶段已经完成了一致性检查,因此本过程直接执行,而不做任何检查。并且在执行的过程中,会使用到 Try 阶段预留的业务资源。

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

假设用户 A 用他的账户余额给用户 B 发一个 100 元的红包,并且余额系统和红包系统是两个独立的系统。

Try

    创建一条转账流水,并将流水的状态设为交易中

    将用户 A 的账户中扣除 100 元(预留业务资源)

    Try 成功之后,便进入 Confirm 阶段

    Try 过程发生任何异常,均进入 Cancel 阶段

Confirm

    向 B 用户的红包账户中增加 100 元

    将流水的状态设为交易已完成

    Confirm 过程发生任何异常,均进入 Cancel 阶段

    Confirm 过程执行成功,则该事务结束

Cancel

    将用户 A 的账户增加 100 元

    将流水的状态设为交易失败

在传统事务机制中,业务逻辑的执行和事务的处理,是在不同的阶段由不同的部件来完成的,业务逻辑部分访问资源实现数据存储,其处理是由业务系统负责;事务处理部分通过协调资源管理器以实现事务管理,其处理由事务管理器来负责。二者没有太多交互的地方,所以,传统事务管理器的事务处理逻辑,仅需要着眼于事务完成(commit/rollback)阶段,而不必关注业务执行阶段。

正文完
 0