• 分布式事务最终一致性的方案


    最终一致性的方案

    知识储备

    分布式系统中不可避免存在分布式事务带来的一致性问题。为了解决这个问题,需要熟悉业界相关的理论:

    • ACID

    • CAP

    • BASE

    • 2PC

    • 3PC

    • TCC

    对于一致性的处理,分为强一致和最终一致性。强一致,对系统的吞吐量和性能有较大损耗,一般用在金融/银行系统,而最终一致性,是以牺牲短期的数据强一致、提升可用性的方案。 对于大部分分布式系统,强烈建议放弃强一致性,采取最终一致性方案。

    跨系统调用存在的问题

    同步调用

    • 现状:微服务之间采用HTTP调用,在一个事务内涉及跨系统调用,未考虑过事务一致性问题

    • 问题:在异常情况下一定出现数据不一致和脏数据

    •  

    异步消息

    • 现状:采用消息队列进行模块解耦,相比第一方案,在吞吐量和可用性方面是更好选择。我们来分析下该方案

    • 问题:出现数据不一致

    场景本地事务消息处理出现原因数据一致
    1本地处理成功消息发送成功一致
    2本地处理成功消息发送失败- 消息服务出问题
    • 消息没有正确投递 | 没有不一致 | | 3 | 本地处理失败 | 消息发送失败 | | 没有不一致 | | 4 | 本地处理失败 | 消息发送成功 | - 发消息客户端超时,消息服务端成功

    • 发消息成功,然后A系统突然挂了 | 不一致 |

    •  

    业界最终一致性方案

    本地消息表

    该方案的核心:

    1. 在发起远程调用前,先将远程调用的上下文持久化到一个消息表中,并要求消息表的操作与业务表的操作在一个本地事务中,然后通过异步机制去做远程调用。

    2. 消息表中维护了远程调用操作的状态机,当远程调用成功后,需要标记状态为成功。

    3. 有一点需要注意:如果遇到异步调用没有成功触发(网络原因或系统down机),需要有补偿重试机制,扫描本地消息表的数据,触发远程调用直到成功。

    该方案实现方式较重,需要在每个使用该方案的业务系统专门维护一张消息表。

     

    外部消息表

    也称可靠型消息。和本地消息表的区别在于,将消息表移到了云端,由消息中间件统一管理消息的状态机,负责消息的初始化、重投、删除。RocketMQ是典型的例子。

     

     

    Seata

    Seata总共提供了4种模式,分别为AT、TCC、SAGA、XA。其中XA是强一致的,性能较差。

    AT

    AT是Seata主推的模式,是基于改进后的二阶段协议实现的。其技术核心是在每个服务的业务数据库中创建一个undolog表。

    1. 在事务第一阶段,Seata确保业务表与undolog表的操作在一个本地事务内。 在undolog表中,会分别记录事务提交前后的数据,称之为前镜像和后镜像,Seata框架会根据前后镜像以及当前SQL的类型,动态分析、计算出反向的回滚SQL。

    2. 在事务二阶段,如果需要提交,则会删除undolog;如果需要回滚,则Seata框架会执行底层自动生成的回滚SQL。

    AT模式不能保证强一致,会存在中间状态,性能较高。AT要求我们拥有每个数据库的管理权,适用于企业内部的系统。

    TCC

    TCC是广为人知的模式,分为try、confirm、cancel三阶段。

    try阶段就是对资源进行预占用,这个就需要对业务模型进行改造,增加中间态字段。

    典型的例子,需要在单据表中增加维护预锁定资源的信息,例如锁定库存、预占用金额等。

    confirm阶段和cancel阶段,将锁定资源释放,刷新实际资源信息,刷新库存、实际金额等。

    TCC不是强一致的,同样存在中间状态的数据。它对业务系统的侵入性很高,所以使用场景比较局限。TCC和AT一样,要求我们拥有每个数据库的管理权。

    SAGA

    SAGA是基于状态机实现的二阶段协议。其原理:针对每个分支事务的正向业务逻辑,都要求提供一个反向的逻辑实现,以便在出现异常时可以调用反向逻辑进行回滚。SAGA的正向逻辑和反向逻辑,都需要程序员去实现,使用成本较高。它比较适用于长事务场景,尤其是涉及和第三方系统进行交互的场景(业务数据库无法由我方管理)。SAGA不是强一致的,同样存在中间态的数据。

    事务消息接入

    对数据一致性有要求的场景,可以使用rocketmq的事务型消息,接入比较简单。

    使用方式

    TransactionMQProducer,区别于发普通消息的DefaultMQProducer

    1. @Override
    2. public TransactionSendResult sendMessageInTransaction(final Message msg,
    3.    final Object arg) throws MQClientException {
    4.    if (null == this.transactionListener) {
    5.        throw new MQClientException( "TransactionListener is null" , null);
    6.   }
    7.    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
    8. }

    TransactionMQProducer初始化时要设置一TransactionListener。

    事务提交和事务回查都在TransactionListener实现。

    1. public interface TransactionListener {
    2.    /**
    3. * When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
    4. * @param msg Half(prepare) message
    5. * @param arg Custom business parameter
    6. * @return Transaction state
    7. */
    8.    LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
    9.    /**
    10. * When no response to prepare(half) message. broker will send check message to check the transaction status, and this
    11. * method will be invoked to get local transaction status.
    12. * @param msg Check message
    13. * @return Transaction state
    14. */
    15. LocalTransactionState checkLocalTransaction(final MessageExt msg);

    事务状态

    public enum LocalTransactionState {      COMMIT_MESSAGE,      ROLLBACK_MESSAGE,      UNKNOW,  } 

    executeLocalTransaction方法

    有两种接入方法:

    • 标准的实现,是将本地的事务逻辑都写在此方法内部,但缺点是对代码的侵入性较大,尤其是当要对老代码进行改造时难度较大

    • 另外一种取巧的方法,本地事务逻辑正常写在其他处,然后在executeLocalTransaction方法中返回UNKNOW 状态,这样就完全依靠回查来决定事务的提交状态。

    checkLocalTransaction方法

    在此方法中汇报本地事务的提交、回滚状态。一般需要通过查询业务表来实现。 以电商系统为例,在订单生成时,发送消息通知物流系统生成物流单据,由于写入订单单据和发送消息要求保证原子性,而本地事务的状态,可以通过判断订单单据是否写入来判断,故checkLocalTransaction的逻辑是:根据订单号查询订单的记录

    • 如果订单记录不存在,表明事务未提交,需返回COMMIT_MESSAGE

    • 如果订单记录存在,表明事务提交,需返回ROLLBACK_MESSAGE

    • 如果方法执行异常,返回UNKNOW,等待rockermq下一次重试回调

    总结

    本文介绍了分布式系统下最终一致性的常用解决方案,包括本地消息表、事务消息、seata的几种事务模式,他们都有对应的场景。

    1. 本地消息表是一个可以满足多数业务要求的场景,可用性较高,如果不希望引入其他中间件,可以考虑该方案。在具体实践中,可以将消息的持久化、异步分发远程调用、补偿重试等共性逻辑封装成组件。

    2. 事务消息有比较广泛的使用场景,稳定性有保障,但由于依赖消息中间件,稳定性不如本地消息表,另外在出现问题时排查不大方便,建议对于链路监控多做考虑。

    3. seata的几种模式本文有详细介绍,在实践中要因地制宜的选择。

  • 相关阅读:
    worthington组织解离指南——上皮组织&结缔组织
    Redis 高性能设计之epoll和IO多路复用深度解析
    【EI会议征稿通知】第十届机械工程、材料和自动化技术国际会议(MMEAT 2024)
    Vagrant+VirtualBox+Docker+MySQL+Redis+Nacos
    虚拟机安装
    Java版分布式微服务云开发架构 Spring Cloud+Spring Boot+Mybatis 电子招标采购系统功能清单
    WebRTC学习笔记七 pion/webrtc
    总结篇:链表
    Tomcat动静分离和负载均衡(多实例试验)
    黑马程序员微服务Docker实用篇
  • 原文地址:https://blog.csdn.net/Andrew_Chenwq/article/details/128180342