精华内容
下载资源
问答
  • 本人最近学习了一下微服务下数据一致性的特点,总结了目前的保障微服务下数据一致性的几种实现方式如下,以备后查。此篇文章旨在给大家一个基于微服务数据一致性实现的大概介绍,并未深入展开,具...

    本人最近学习了一下微服务下数据一致性的特点,总结了下目前的保障微服务下数据一致性的几种实现方式如下,以备后查。此篇文章旨在给大家一个基于微服务的数据一致性实现的大概介绍,并未深入展开,具体的实现方式本人也在继续学习中,如有错误,欢迎大家拍砖。

    传统应用的事务管理

    本地事务

    在介绍微服务下的数据一致性之前,先简单地介绍一下事务的背景。传统单机应用使用一个RDBMS作为数据源。应用开启事务,进行CRUD,提交或回滚事务,统统发生在本地事务中,由资源管理器(RM)直接提供事务支持。数据的一致性在一个本地事务中得到保证。

    分布式事务

    两阶段提交(2PC)

    当应用逐渐扩展,出现一个应用使用多个数据源的情况,这个时候本地事务已经无法满足数据一致性的要求。由于多个数据源的同时访问,事务需要跨多个数据源管理,分布式事务应运而生。其中最流行的就是两阶段提交(2PC),分布式事务由事务管理器(TM)统一管理。

    两阶段提交分为准备阶段和提交阶段。

    两阶段提交-commit

    两阶段提交-rollback

    然而两阶段提交也不能完全保证数据一致性问题,并且有同步阻塞的问题,所以其优化版本三阶段提交(3PC)被发明了出来。

    三阶段提交(3PC)

    三阶段提交

    然而3PC也只能保证绝大多数情况下的数据一致性。

    微服务下的事务管理

    那么,分布式事务2PC或者3PC是否适合于微服务下的事务管理呢?答案是否定的,原因有三点:

    • 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC(Dubbo)或Http API(Spring Cloud)进行,所以已经无法使用TM统一管理微服务的RM。

    • 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不支持事务的数据库,则事务根本无从谈起。

    • 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能。

    由此可见,传统的分布式事务已经无法满足微服务架构下的事务管理需求。那么,既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则--BASE理论。

    BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

    • 基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

    • 软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。

    • 最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

    BASE中的最终一致性是对于微服务下的事务管理的根本要求,既基于微服务的事务管理无法达到强一致性,但必须保证最重一致性。那么,有哪些方法可以保证微服务下的事务管理的最终一致性呢,按照实现原理分主要有两类,事件通知型和补偿型,其中事件通知型又可分为可靠事件通知模式及最大努力通知模式,而补偿型又可分为TCC模式、和业务补偿模式两种。这四种模式都可以达到微服务下的数据最终一致性。

    实现微服务下数据一致性的方式

    可靠事件通知模式

    同步事件

    可靠事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。首先能想到的也是最简单的就是同步事件通知,业务处理与消息发送同步执行,实现逻辑见下方代码及时序图。

    public void trans() {
        try {
        // 1. 操作数据库
            bool result = dao.update(data);// 操作数据库失败,会抛出异常
        // 2. 如果数据库操作成功则发送消息
            if(result){
                mq.send(data);// 如果方法执行失败,会抛出异常
            }
        } catch (Exception e) {
            roolback();// 如果发生异常,就回滚
        }
    }
    

    上面的逻辑看上去天衣无缝,如果数据库操作失败则直接退出,不发送消息;如果发送消息失败,则数据库回滚;如果数据库操作成功且消息发送成功,则业务成功,消息发送给下游消费。然后仔细思考后,同步消息通知其实有两点不足的地方。

    1. 在微服务的架构下,有可能出现网络IO问题或者服务器宕机的问题,如果这些问题出现在时序图的第7步,使得消息投递后无法正常通知主服务(网络问题),或无法继续提交事务(宕机),那么主服务将会认为消息投递失败,会滚主服务业务,然而实际上消息已经被从服务消费,那么就会造成主服务和从服务的数据不一致。具体场景可见下面两张时序图。

    2. 事件服务(在这里就是消息服务)与业务过于耦合,如果消息服务不可用,会导致业务不可用。应该将事件服务与业务解耦,独立出来异步执行,或者在业务执行后先尝试发送一次消息,如果消息发送失败,则降级为异步发送。

    异步事件

    本地事件服务:

    为了解决上述同步事件中描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。

    异步事件通知——本地事件服务

    当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。如果投递失败,则使用事件服务定时地异步统一处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用异步事件服务保证事件至少被投递一次。

    然而,这种使用本地事件服务保证可靠事件通知的方式也有它的不足之处,那便是业务仍旧与事件服务有一定耦合(第一次同步投递时),更为严重的是,本地事务需要负责额外的事件表的操作,为数据库带来了压力,在高并发的场景,由于每一个业务操作就要产生相应的事件表操作,几乎将数据库的可用吞吐量砍了一半,这无疑是无法接受的。正是因为这样的原因,可靠事件通知模式进一步地发展-外部事件服务出现在了人们的眼中。

    外部事件服务:

    外部事件服务在本地事件服务的基础上更进了一步,将事件服务独立出主业务服务,主业务服务不在对事件服务有任何强依赖。

    异步事件通知——外部事件服务

    业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。

    外部事件虽然能够将业务系统和事件系统解耦,但是也带来了额外的工作量:外部事件服务比起本地事件服务来说多了两次网络通信开销(提交前、提交/回滚后),同时也需要业务系统提供单独的查询接口给事件系统用来判断未发送事件的状态。

    可靠事件通知模式的注意事项:

    可靠事件模式需要注意的有两点:1. 事件的正确发送;2. 事件的重复消费。

    通过异步消息服务可以确保事件的正确发送,然而事件是有可能重复发送的,那么就需要消费端保证同一条事件不会重复被消费,简而言之就是保证事件消费的幂等性。

    如果事件本身是具备幂等性的状态型事件,如订单状态的通知(已下单、已支付、已发货等),则需要判断事件的顺序。一般通过时间戳来判断,既消费过了新的消息后,当接受到老的消息直接丢弃不予消费。如果无法提供全局时间戳,则应考虑使用全局统一的序列号。

    对于不具备幂等性的事件,一般是动作行为事件,如扣款100,存款200,则应该将事件ID及事件结果持久化,在消费事件前查询事件ID,若已经消费则直接返回执行结果;若是新消息,则执行,并存储执行结果。

    最大努力通知模式

    相比可靠事件通知模式,最大努力通知模式就容易理解多了。最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等,这里不再展开。

    业务补偿模式

    接下来介绍两种补偿模式,补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。首先介绍业务补偿模式,业务补偿模式是一种纯补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。举个例子,小明从杭州出发,去往美国纽约出差,现在他需要定从杭州去往上海的火车票,以及从上海飞往纽约的飞机票。如果小明成功购买了火车票之后发现那天的飞机票已经售空了,那么与其在上海再多待一天,小明还不如取消去上海的火车票,选择飞往北京再转机纽约,所以小明就取消了去上海的火车票。这个例子中购买杭州到上海的火车票是服务a,购买上海到纽约的飞机票是服务b,业务补偿模式就是在服务b失败的时候,对服务a进行补偿操作,在例子中就是取消杭州到上海的火车票。

    补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。

    业务补偿模式最大的缺点是软状态的时间比较长,既数据一致性的时效性很低,多个服务常常可能处于数据不一致的情况。

    TCC/Try Confirm Cancel模式

    TCC模式是一种优化了的业务补偿模式,它可以做到完全补偿,既进行补偿后不留下补偿的纪录,就好像什么事情都没有发生过一样。同时,TCC的软状态时间很短,原因是因为TCC是一种两阶段型模式,只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。

    TCC模式

    TCC模式的具体流程为两个阶段:

    1. Try,业务服务完成所有的业务检查,预留必需的业务资源

    2. 如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。

    这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。

    服务a(小明从招行转出100元):

    try:update cmb_account set balance=balance-100, freeze=freeze+100 where acc_id=1 and balance>100;

    confirm:update cmb_account set freeze=freeze-100 where acc_id=1;

    cancel:update cmb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;

    服务b(小明往广发银行汇入100元):

    try:update cgb_account set freeze=freeze+100 where acc_id=1;

    confirm:update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;

    cancel:update cgb_account set freeze=freeze-100 where acc_id=1;

    具体说明:

    a的try阶段,服务做了两件事:1. 业务检查,这里是检查小明的帐户里的钱是否多余100元;2. 预留资源,将100元从余额中划入冻结资金。

    a的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。

    a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。

    b的try阶段进行,预留资源,将100元冻结。

    b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。

    b的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。

    从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。

    总结

    下面的表格对这四种常用的模式进行了比较:

    原文链接:https://www.jianshu.com/p/b264a196b177

    展开全文
  • 如何保证微服务下数据一致性?

    万次阅读 2020-09-15 10:50:35
    3、实现微服务下数据一致性的方式 3.1 可靠事件通知模式 3.1.1 同步事件 3.1.2 异步事件 3.1.2.1 本地事件服务 3.1.2.2 外部事件服务 3.1.2.3 可靠事件通知模式的注意事项 3.2 最大努力通知模式 3.3 业务补偿模式 ...

    目录

    1、传统应用的事务管理
    1.1 本地事务
    1.2 分布式事务
    1.2.1 两阶段提交(2PC)
    1.2.2 三阶段提交(3PC)

    2、微服务下的事务管理

    3、实现微服务下数据一致性的方式
    3.1 可靠事件通知模式
    3.1.1 同步事件
    3.1.2 异步事件
    3.1.2.1 本地事件服务
    3.1.2.2 外部事件服务
    3.1.2.3 可靠事件通知模式的注意事项
    3.2 最大努力通知模式
    3.3 业务补偿模式
    3.4 TCC/Try Confirm Cancel模式
    3.5 总结

    1. 传统应用的事务管理

    1.1 本地事务

    再介绍微服务下的数据一致性之前,先简单地介绍一下事务的背景。传统单机应用使用一个RDBMS作为数据源。应用开启事务,进行CRUD,提交或回滚事务,统统发生在本地事务中,由资源管理器(RM)直接提供事务支持。数据的一致性在一个本地事务中得到保证。

    image-20200915100224763

    1.2 分布式事务

    1.2.1 两阶段提交(2PC)

    当应用逐渐扩展,出现一个应用使用多个数据源的情况,这个时候本地事务已经无法满足数据一致性的要求。由于多个数据源的同时访问,事务需要跨多个数据源管理,分布式事务应运而生。其中最流行的就是两阶段提交(2PC),分布式事务由事务管理器(TM)统一管理。

    两阶段提交分为准备阶段和提交阶段。

    image-20200915100239121

    两阶段提交-commit

    image-20200915100303452

    两阶段提交-rollback

    然而两阶段提交也不能完全保证数据一致性问题,并且有同步阻塞的问题,所以其优化版本三阶段提交(3PC)被发明了出来。

    1.2.2 三阶段提交(3PC)

    image-20200915100342933

    三阶段提交

    然而3PC也只能保证绝大多数情况下的数据一致性。

    2. 微服务下的事务管理

    那么,分布式事务2PC或者3PC是否适合于微服务下的事务管理呢?答案是否定的,原因有三点:

    1. 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC(dubbo)或Http API(SpringCloud)进行,所以已经无法使用TM统一管理微服务的RM。
    2. 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不支持事务的数据库,则事务根本无从谈起。
    3. 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能。

    由此可见,传统的分布式事务已经无法满足微服务架构下的事务管理需求。那么,既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则--BASE理论。

    BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

    基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

    软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。

    最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

    BASE中的最终一致性是对于微服务下的事务管理的根本要求,既基于微服务的事务管理无法达到强一致性,但必须保证最重一致性。那么,有哪些方法可以保证微服务下的事务管理的最终一致性呢,按照实现原理分主要有两类,事件通知型和补偿型,其中事件通知型又可分为可靠事件通知模式及最大努力通知模式,而补偿型又可分为TCC模式、和业务补偿模式两种。这四种模式都可以达到微服务下的数据最终一致性。

    3. 实现微服务下数据一致性的方式

    3.1 可靠事件通知模式

    3.1.1 同步事件

    可靠事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。首先能想到的也是最简单的就是同步事件通知,业务处理与消息发送同步执行,实现逻辑见下方代码及时序图。

    public void trans() {
        try {
        // 1. 操作数据库
            bool result = dao.update(data);// 操作数据库失败,会抛出异常
        // 2. 如果数据库操作成功则发送消息
            if(result){
                mq.send(data);// 如果方法执行失败,会抛出异常
            }
        } catch (Exception e) {
            roolback();// 如果发生异常,就回滚
        }
    }
    

    image-20200915100937990

    上面的逻辑看上去天衣无缝,如果数据库操作失败则直接退出,不发送消息;如果发送消息失败,则数据库回滚;如果数据库操作成功且消息发送成功,则业务成功,消息发送给下游消费。然后仔细思考后,同步消息通知其实有两点不足的地方。

    1. 在微服务的架构下,有可能出现网络IO问题或者服务器宕机的问题,如果这些问题出现在时序图的第7步,使得消息投递后无法正常通知主服务(网络问题),或无法继续提交事务(宕机),那么主服务将会认为消息投递失败,会滚主服务业务,然而实际上消息已经被从服务消费,那么就会造成主服务和从服务的数据不一致。具体场景可见下面两张时序图。

      image-20200915101107840

    image-20200915101139561

    1. 事件服务(在这里就是消息服务)与业务过于耦合,如果消息服务不可用,会导致业务不可用。应该将事件服务与业务解耦,独立出来异步执行,或者在业务执行后先尝试发送一次消息,如果消息发送失败,则降级为异步发送。

    3.1.2 异步事件

    3.1.2.1 本地事件服务

    为了解决3.1.1中描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。

    image-20200915101203042

    异步事件通知-本地事件服务

    当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。如果投递失败,则使用事件服务定时地异步统一处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用异步事件服务保证事件至少被投递一次。

    然而,这种使用本地事件服务保证可靠事件通知的方式也有它的不足之处,那便是业务仍旧与事件服务有一定耦合(第一次同步投递时),更为严重的是,本地事务需要负责额外的事件表的操作,为数据库带来了压力,在高并发的场景,由于每一个业务操作就要产生相应的事件表操作,几乎将数据库的可用吞吐量砍了一半,这无疑是无法接受的。正是因为这样的原因,可靠事件通知模式进一步地发展-外部事件服务出现在了人们的眼中。

    3.1.2.2 外部事件服务

    外部事件服务在本地事件服务的基础上更进了一步,将事件服务独立出主业务服务,主业务服务不在对事件服务有任何强依赖。

    image-20200915101219793

    异步事件通知-外部事件服务

    业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。

    外部事件虽然能够将业务系统和事件系统解耦,但是也带来了额外的工作量:外部事件服务比起本地事件服务来说多了两次网络通信开销(提交前、提交/回滚后),同时也需要业务系统提供单独的查询接口给事件系统用来判断未发送事件的状态。

    3.1.2.3 可靠事件通知模式的注意事项

    可靠事件模式需要注意的有两点,1. 事件的正确发送; 2. 事件的重复消费。
    通过异步消息服务可以确保事件的正确发送,然而事件是有可能重复发送的,那么就需要消费端保证同一条事件不会重复被消费,简而言之就是保证事件消费的幂等性

    如果事件本身是具备幂等性的状态型事件,如订单状态的通知(已下单、已支付、已发货等),则需要判断事件的顺序。一般通过时间戳来判断,既消费过了新的消息后,当接受到老的消息直接丢弃不予消费。如果无法提供全局时间戳,则应考虑使用全局统一的序列号。

    对于不具备幂等性的事件,一般是动作行为事件,如扣款100,存款200,则应该将事件id及事件结果持久化,在消费事件前查询事件id,若已经消费则直接返回执行结果;若是新消息,则执行,并存储执行结果。

    3.2 最大努力通知模式

    相比可靠事件通知模式,最大努力通知模式就容易理解多了。最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等,这里不再展开。

    3.3 业务补偿模式

    接下来介绍两种补偿模式,补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。首先介绍业务补偿模式,业务补偿模式是一种纯补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。举个例子,小明从杭州出发,去往美国纽约出差,现在他需要定从杭州去往上海的火车票,以及从上海飞往纽约的飞机票。如果小明成功购买了火车票之后发现那天的飞机票已经售空了,那么与其在上海再多待一天,小明还不如取消去上海的火车票,选择飞往北京再转机纽约,所以小明就取消了去上海的火车票。这个例子中购买杭州到上海的火车票是服务a,购买上海到纽约的飞机票是服务b,业务补偿模式就是在服务b失败的时候,对服务a进行补偿操作,在例子中就是取消杭州到上海的火车票。

    补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。

    业务补偿模式最大的缺点是软状态的时间比较长,既数据一致性的时效性很低,多个服务常常可能处于数据不一致的情况。

    3.4 TCC/Try Confirm Cancel模式

    TCC模式是一种优化了的业务补偿模式,它可以做到完全补偿,既进行补偿后不留下补偿的纪录,就好像什么事情都没有发生过一样。同时,TCC的软状态时间很短,原因是因为TCC是一种两阶段型模式(已经忘了两阶段概念的可以回顾一下1.2.1),只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。

    image-20200915101238858

    TCC模式

    TCC模式的具体流程为两个阶段:

    1. Try,业务服务完成所有的业务检查,预留必需的业务资源
    2. 如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。

    这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。

    服务a(小明从招行转出100元):

    try: update cmb_account set balance=balance-100, freeze=freeze+100 where acc_id=1 and balance>100;

    confirm: update cmb_account set freeze=freeze-100 where acc_id=1;

    cancel: update cmb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;

    服务b(小明往广发银行汇入100元):

    try: update cgb_account set freeze=freeze+100 where acc_id=1;

    confirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;

    cancel: update cgb_account set freeze=freeze-100 where acc_id=1;

    具体说明:
    a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。

    a的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。

    a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。

    b的try阶段进行,预留资源,将100元冻结。

    b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。

    b的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。

    从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。

    3.5 总结

    下面的表格对这四种常用的模式进行了比较:

    类型名称数据一致性的实时性开发成本上游服务是否依赖下游服务结果
    通知型最大努力不依赖
    通知型可靠事件不依赖
    补偿型业务补偿依赖
    补偿型TCC依赖

    如果大家对java架构相关感兴趣,可以关注下面公众号,会持续更新java基础面试题, netty, spring boot,spring cloud等系列文章,一系列干货随时送达, 超神之路从此展开, BTAJ不再是梦想!

    架构殿堂

    展开全文
  • 目录 1. 传统应用的事务管理 1.1 本地事务 1.2 分布式事务 2. 微服务下的事务管理 3. 实现微服务下数据一致性的方式 3.1 可靠事件通知模式 ...再介绍微服务下数据一致性之前,先简单地介绍一...

    目录

     

    1. 传统应用的事务管理

    1.1 本地事务

    1.2 分布式事务

    2. 微服务下的事务管理

    3. 实现微服务下数据一致性的方式

    3.1 可靠事件通知模式

    3.2 最大努力通知模式

    3.3 业务补偿模式

    3.4 TCC/Try Confirm Cancel模式

    3.5 总结


    1. 传统应用的事务管理

    1.1 本地事务

    再介绍微服务下的数据一致性之前,先简单地介绍一下事务的背景。传统单机应用使用一个RDBMS作为数据源。应用开启事务,进行CRUD,提交或回滚事务,统统发生在本地事务中,由资源管理器(RM)直接提供事务支持。数据的一致性在一个本地事务中得到保证。

     

    1.2 分布式事务

    1.2.1 两阶段提交(2PC)

    当应用逐渐扩展,出现一个应用使用多个数据源的情况,这个时候本地事务已经无法满足数据一致性的要求。由于多个数据源的同时访问,事务需要跨多个数据源管理,分布式事务应运而生。其中最流行的就是两阶段提交(2PC),分布式事务由事务管理器(TM)统一管理。

    两阶段提交分为准备阶段和提交阶段。

     


    两阶段提交-commit

     


    两阶段提交-rollback

     

    然而两阶段提交也不能完全保证数据一致性问题,并且有同步阻塞的问题,所以其优化版本三阶段提交(3PC)被发明了出来。

    1.2.2 三阶段提交(3PC)

    然而3PC也只能保证绝大多数情况下的数据一致性。

    具体分布式事务2PC和3PC的详细介绍请见关于分布式事务、两阶段提交协议、三阶提交协议。分布式事务不是本文的重点,故不展开。

    2. 微服务下的事务管理

    那么,分布式事务2PC或者3PC是否适合于微服务下的事务管理呢?答案是否定的,原因有三点:

    1. 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC(dubbo)或Http API(SpringCloud)进行,所以已经无法使用TM统一管理微服务的RM。
    2. 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不支持事务的数据库,则事务根本无从谈起。
    3. 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能。

    由此可见,传统的分布式事务已经无法满足微服务架构下的事务管理需求。那么,既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则--BASE理论。

    BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

    基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

    软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。

    最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

    BASE中的最终一致性是对于微服务下的事务管理的根本要求,既基于微服务的事务管理无法达到强一致性,但必须保证最重一致性。那么,有哪些方法可以保证微服务下的事务管理的最终一致性呢,按照实现原理分主要有两类,事件通知型和补偿型,其中事件通知型又可分为可靠事件通知模式及最大努力通知模式,而补偿型又可分为TCC模式、和业务补偿模式两种。这四种模式都可以达到微服务下的数据最终一致性。

    3. 实现微服务下数据一致性的方式

    3.1 可靠事件通知模式

    3.1.1 同步事件

    可靠事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。首先能想到的也是最简单的就是同步事件通知,业务处理与消息发送同步执行,实现逻辑见下方代码及时序图。

    public void trans() {
        try {
        // 1. 操作数据库
            bool result = dao.update(data);// 操作数据库失败,会抛出异常
        // 2. 如果数据库操作成功则发送消息
            if(result){
                mq.send(data);// 如果方法执行失败,会抛出异常
            }
        } catch (Exception e) {
            roolback();// 如果发生异常,就回滚
        }
    }
    

     

    上面的逻辑看上去天衣无缝,如果数据库操作失败则直接退出,不发送消息;如果发送消息失败,则数据库回滚;如果数据库操作成功且消息发送成功,则业务成功,消息发送给下游消费。然后仔细思考后,同步消息通知其实有两点不足的地方。

           1. 在微服务的架构下,有可能出现网络IO问题或者服务器宕机的问题,如果这些问题出现在时序图的第4步,使得消息投递后无法正常通知主服务,那么主服务将会认为消息投递失败,会滚主服务业务,然而实际上消息已经被从服务消费,那么就会造成主服务和从服务的数据不一致。具体场景可见下面两张时序图。

    1.  

     

          2. 事件服务(在这里就是消息服务)与业务过于耦合,如果消息服务不可用,会导致业务不可用。应该将事件服务与业务解耦,独立出来异步执行,或者在业务执行后先尝试发送一次消息,如果消息发送失败,则降级为异步发送。

    3.1.2 异步事件

    3.1.2.1 本地事件服务

    为了解决3.1.1中描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。

     


    异步事件通知-本地事件服务

     

    当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。如果投递失败,则使用事件服务定时地异步统一处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用异步事件服务保证事件至少被投递一次。

    然而,这种使用本地事件服务保证可靠事件通知的方式也有它的不足之处,那便是业务仍旧与事件服务有一定耦合(第一次同步投递时),更为严重的是,本地事务需要负责额外的事件表的操作,为数据库带来了压力,在高并发的场景,由于每一个业务操作就要产生相应的事件表操作,几乎将数据库的可用吞吐量砍了一半,这无疑是无法接受的。正是因为这样的原因,可靠事件通知模式进一步地发展-外部事件服务出现在了人们的眼中。

    3.1.2.2 外部事件服务

    外部事件服务在本地事件服务的基础上更进了一步,将事件服务独立出主业务服务,主业务服务不在对事件服务有任何强依赖。

     

     


    异步事件通知-外部事件服务

     

    业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。

    外部事件虽然能够将业务系统和事件系统解耦,但是也带来了额外的工作量:外部事件服务比起本地事件服务来说多了两次网络通信开销(提交前、提交/回滚后),同时也需要业务系统提供单独的查询接口给事件系统用来判断未发送事件的状态。

    3.1.2.3 可靠事件通知模式的注意事项

    可靠事件模式需要注意的有两点,1. 事件的正确发送; 2. 事件的重复消费。
    通过异步消息服务可以确保事件的正确发送,然而事件是有可能重复发送的,那么就需要消费端保证同一条事件不会重复被消费,简而言之就是保证事件消费的幂等性

    如果事件本身是具备幂等性的状态型事件,如订单状态的通知(已下单、已支付、已发货等),则需要判断事件的顺序。一般通过时间戳来判断,既消费过了新的消息后,当接受到老的消息直接丢弃不予消费。如果无法提供全局时间戳,则应考虑使用全局统一的序列号。

    对于不具备幂等性的事件,一般是动作行为事件,如扣款100,存款200,则应该将事件id及事件结果持久化,在消费事件前查询事件id,若已经消费则直接返回执行结果;若是新消息,则执行,并存储执行结果。

    3.2 最大努力通知模式

    相比可靠事件通知模式,最大努力通知模式就容易理解多了。最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等,这里不再展开。

    3.3 业务补偿模式

    接下来介绍两种补偿模式,补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。首先介绍业务补偿模式,业务补偿模式是一种纯补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。举个例子,小明从杭州出发,去往美国纽约出差,现在他需要定从杭州去往上海的火车票,以及从上海飞往纽约的飞机票。如果小明成功购买了火车票之后发现那天的飞机票已经售空了,那么与其在上海再多待一天,小明还不如取消去上海的火车票,选择飞往北京再转机纽约,所以小明就取消了去上海的火车票。这个例子中购买杭州到上海的火车票是服务a,购买上海到纽约的飞机票是服务b,业务补偿模式就是在服务b失败的时候,对服务a进行补偿操作,在例子中就是取消杭州到上海的火车票。

    补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。

    业务补偿模式最大的缺点是软状态的时间比较长,既数据一致性的时效性很低,多个服务常常可能处于数据不一致的情况。

    3.4 TCC/Try Confirm Cancel模式

    TCC模式是一种优化了的业务补偿模式,它可以做到完全补偿,既进行补偿后不留下补偿的纪录,就好像什么事情都没有发生过一样。同时,TCC的软状态时间很短,原因是因为TCC是一种两阶段型模式(已经忘了两阶段概念的可以回顾一下1.2.1),只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。

     


    TCC模式

     

    TCC模式的具体流程为两个阶段:

    1. Try,业务服务完成所有的业务检查,预留必需的业务资源
    2. 如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。

    这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。

    服务a(小明从招行转出100元):

    try:

    update cmb_account set balance=balance-100, freeze=freeze+100 where acc_id=1 and balance>100;

    confirm:

    update cmb_account set freeze=freeze-100 where acc_id=1;

    cancel:

    update cmb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;

    服务b(小明往广发银行汇入100元):

    try:

    update cgb_account set freeze=freeze+100 where acc_id=1;

    confirm:

    update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;

    cancel:

    update cgb_account set freeze=freeze-100 where acc_id=1;

    具体说明:


    a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。

    a的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。

    a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。

    b的try阶段进行,预留资源,将100元冻结。

    b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。

    b的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。

    从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。

    3.5 总结

    下面的表格对这四种常用的模式进行了比较:

    类型名称数据一致性的实时性开发成本上游服务是否依赖下游服务结果
    通知型最大努力不依赖
    通知型可靠事件不依赖
    补偿型业务补偿依赖
    补偿型TCC依赖



    作者:SawyerZhou
    链接:https://www.jianshu.com/p/b264a196b177
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • Netflix/Airbnb等一线互联网公司的实践[参考附录1/2/3]表明,数据一致性分发能力,是构建松散耦合、可扩展和高性能的微服务架构的基础。 本文解释分布式微服务中的数据一致性分发问题,应用场景,并给出常见的解决...

    介绍

    系统架构微服务化以后,根据微服务独立数据源的思想,每个微服务一般具有各自独立的数据源,但是不同微服务之间难免需要通过数据分发来共享一些数据,这个就是微服务的数据分发问题。Netflix/Airbnb等一线互联网公司的实践[参考附录1/2/3]表明,数据一致性分发能力,是构建松散耦合、可扩展和高性能的微服务架构的基础。

    本文解释分布式微服务中的数据一致性分发问题,应用场景,并给出常见的解决方法。本文主要面向互联网分布式系统架构师和研发经理。

    为啥要分发数据?场景?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyluATQu-1593683238146)(images/scenarios.png)]

    我们还是要从具体业务场景出发,为啥要分发数据?有哪些场景?在实际企业中,数据分发的场景其实是非常多的。假设某电商企业有这样一个订单服务Order Service,它有一个独立的数据库。同时,周边还有不少系统需要订单的数据,上图给出了一些例子:

    1. 一个是缓存系统,为了提升订单数据的访问性能,我们可以把频繁访问的订单数据,通过Redis缓存起来;
    2. 第二个是Fulfillment Service,也就是订单履行系统,它也需要一份订单数据,借此实现订单履行的功能;
    3. 第三个是ElasticSearch搜索引擎系统,它也需要一份订单数据,可以支持前台用户、或者是后台运营快速查询订单信息;
    4. 第四个是传统数据仓库系统,它也需要一份订单数据,支持对订单数据的分析和挖掘。

    当然,为了获得一份订单数据,这些系统可以定期去订单服务查询最新的数据,也就是拉模式,但是拉模式有两大问题:

    1. 一个是拉数据通常会有延迟,也就是说拉到的数据并不实时;
    2. 如果频繁拉的话,考虑到外围系统众多(而且可能还会增加),势必会对订单数据库的性能造成影响,严重时还可能会把订单数据库给拉挂。

    所以,当企业规模到了一定阶段,还是需要考虑数据分发技术,将业务数据同步分发到对数据感兴趣的其它服务。除了上面提到的一些数据分发场景,其实还有很多其它场景,例如:

    1. 第一个是数据复制(replication)。为了实现高可用,一般要将数据复制多分存储,这个时候需要采用数据分发。
    2. 第二个是支持数据库的解耦拆分。在单体数据库解耦拆分的过程中,为了实现不停机拆分,在一段时间内,需要将遗留老数据同步复制到新的数据存储,这个时候也需要数据分发技术。
    3. 第三个是实现CQRS,还有去数据库Join。这两个场景我后面有单独文章解释,这边先说明一下,实现CQRS和数据库去Join的底层技术,其实也是数据分发。
    4. 第四个是实现分布式事务。这个场景我后面也有单独文章讲解,这边先说明一下,解决分布式事务问题的一些方案,底层也是依赖于数据分发技术的。
    5. 其它还有流式计算、大数据BI/AI,还有审计日志和历史数据归档等场景,一般都离不开数据分发技术。

    总之,波波认为,数据分发,是构建现代大规模分布式系统、微服务架构和异步事件驱动架构的底层基础技术。

    双写?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T9zA2Xog-1593683238152)(images/dual_writes.png)]

    对于数据分发这个问题,乍一看,好像并不复杂,稍有开发经验的同学会说,我在应用层做一个双写不就可以了吗?比方说,请看上图右边,这里有一个微服务A,它需要把数据写入DB,同时还要把数据写到MQ,对于这个需求,我在A服务中弄一个双写,不就搞定了吗?其实这个问题并没有那么简单,关键是你如何才能保证双写的事务性?

    请看上图左边的代码,这里有一个方法updateDbThenSendMsgInTransaction,这个方法上加了事务性标注,也就是说,如果抛异常的话,数据库操作会回滚。我们来看这个方法的执行步骤:

    第一步先更新数据库,如果更新成功,那么result设为true,如果更新失败,那么result设为false;

    第二步,如果result为true,也就是说DB更新成功,那么我们就继续做第三步,向mq发送消息

    如果发消息也成功,那么我们的流程就走到第四步,整个双写事务就成功了。

    如果发消息抛异常,也就是发消息失败,那么容器会执行该方法的事务性回滚,上面的数据库更新操作也会回滚。

    初看这个双写流程没有问题,可以保证事务性。但是深入研究会发现它其实是有问题的。比方说在第三步,如果发消息抛异常了,并不保证说发消息失败了,可能只是由于网络异常抖动而造成的抛异常,实际消息可能是已经发到MQ中,但是抛异常会造成上面数据库更新操作的回滚,结果造成两边数据不一致。

    模式一:事务性发件箱(Transactional Outbox)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lcmbd5Ur-1593683238155)(images/transactional_outbox.png)]

    对于事务性双写这个问题,业界沉淀下来比较实践的做法,其中一种,就是采用所谓事务性发件箱模式,英文叫Transactional Outbox。据说这个模式是eBay最早发明和使用的。事务性发件箱模式不难理解,请看上图。

    我们仍然以订单Order服务为例。在数据库中,除了订单Order表,为了实现事务性双写,我们还需增加了一个发件箱Outbox表。Order表和Outbox表都在同一个数据库中,对它们进行同时更新的话,通过数据库的事务机制,是可以实现事务性更新的。

    下面我们通过例子来展示这个流程,我们这里假定Order Service要添加一个新订单。

    首先第一步,Order Service先将新订单数据写入Order表,然后它再向Outbox表中写入一条订单新增记录,这两个DB操作可以包在一个DB事务里头,也就是可以实现事务性写入。

    然后第二步,我们再引入一个称为消息中继Message Relay的角色,它负责定期Poll拉取Outbox中的新数据,然后第三步再Publish发送到MQ。如果写入MQ确认成功,Message Relay就可以将Outbox中的对应记录标记为已消费。这里可能会出现一种异常情况,就是Message Relay在将消息发送到MQ时,发生了网络抖动,实际消息可能已经写入MQ,但是Message Relay并没有得到确认,这时候它会重发,直到明确成功为止。所以,这里也是一个At Least Once,也就是至少交付一次的消费语义,消息可能被重复投递。因此,MQ之后的消费方要做消息去重或幂等处理。

    总之,事务性发件箱模式可以保证,对Order表的修改,然后将对应事件发送到MQ,这两个动作可以实现事务性,也就是实现数据分发的事务性。

    注意,这里的Message Relay角色既可以是一个独立部署的服务,也可以和Order Service住在一起。生产实践中,需要考虑Message Relay的高可用部署,还有监控和告警,否则如果Message Relay挂了,消息就发不出来,然后,依赖于消息的各种消费方也将无法正常工作。

    Transactional Outbox参考实现 ~ Killbill Common Queue

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LgbMBEJq-1593683238159)(images/killbill-queue.png)]

    事务性发件箱的原理简单,实现起来也不复杂,波波这边推荐一个生产级的参考实现。这个实现源于一个叫killbill的项目,killbill是美国高朋(GroupOn)公司开源的订阅计费和支付平台,这个项目已经有超过8~9年的历史,在高朋等公司已经有不少落地案例,是一个比较成熟的产品。killbill项目里头有一些公共库,单独放在一个叫killbill-commons的子项目里头,其中有一个叫killbill common queue,它其实是事务性发件箱的一个生产级实现。上图有给出这个queue的github链接。

    Killbill common queue也是一个基于DB实现的分布式的队列,它上层还包装了EventBus事件总线机制。killbill common queue的总体设计思路不难理解,请看上图:

    在上图的左边,killbill common queue提供发送消息API,并且是支持事务的。比方说图上的postFromTransaction方法,它可以发送一个BusEvent事件到DB Queue当中,这个方法还接受一个数据库连接Connection参数,killbill common queue可以保证对事件event的数据库写入,和使用同一个Connection的其它数据库写入操作,发生在同一个事务中。这个做法其实就是一种事务性发件箱的实现,这里的发件箱存的就是事件event。

    除了POST写入API,killbill common queue还支持类似前面提到的Message Relay的功能,并且是包装成EeventBus + Handler方式来实现的。开发者只需要实现事件处理器,并且注册订阅在EventBus上,就可以接收到DB Queue,也就是发件箱当中的新事件,并进行消费处理。如果事件处理成功,那么EvenbBus会将对应的事件从发件箱中移走;如果事件处理不成功,那么EventBus会负责重试,直到处理成功,或者超过最大重试次数,那么它会将该事件标记为处理失败,并移到历史归档表中,等待后续人工检查和干预。这个EventBus的底层,其实有一个Dispatcher派遣线程,它负责定期扫描DB Queue(也就是发件箱)中的新事件,有的话就批量拉取出来,并发送到内部EventBus的队列中,如果内部队列满了,那么Dispather Thread也会暂停拉取新事件。

    在killbill common queue的设计中,每个节点上的Dispather线程只负责通过自己这个节点写入的事件,并且在一个节点上,Dispather线程也只有一个,这样才能保证消息消费的顺序性,并且也不会重复消费。

    Reaper机制

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dxBnwSHW-1593683238160)(images/reaper.png)]

    killbill common queue,其实是一个基于集中式数据库实现的分布式队列,为什么说它是分布式队列呢?请看上图,killbill common queue的设计是这样的,它的每个节点,只负责消费处理从自己这个节点写入的事件。比方说上图中有蓝色/黄色和绿色3个节点,那么蓝色节点,只负责从蓝色节点写入,在数据库中标记为蓝色的事件。同样,黄色节点,只负责从黄色节点写入,在数据库中标记为黄色的事件。绿色节点也是类似。这是一种分布式的设计,如果处理容量不够,只需按需添加更多节点,就可以实现负载分摊。

    这里有个问题,如果其中某个节点挂了,比方说上图的蓝色节点挂了,那么谁来继续消费数据库中蓝色的,还没有来得及处理的事件呢?为了解决这个问题,killbill common queue设计了一种称为reaper收割机的机制。每个节点上都还住了一个收割机线程,它们会定期检查数据库,看有没有长时间无人处理的事件,如果有,就抢占标记为由自己负责。比方说上图的右边,最终黄色节点上的收割机线程抢到了原来由蓝色节点负责的事件,那么它会把这些事件标记为黄色,也就是由自己来负责。

    收割机机制,保证了killbill common queue的高可用性,相当于保证了事务性发件箱中的Message Relay的高可用性。

    Killbill PersistentBus表结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O72AUVht-1593683238162)(images/tables.png)]

    基于killbill common queue的EventBus,也被称为killbill PersistentBus。上图给出了它的数据库表结构,其中bus_events就是用来存放待处理事件的,相当于发件箱,主要的字段包括:

    1. event_json,存放json格式的原始数据。
    2. creating_owner,记录创建节点,也就是事件是由哪个节点写入的。
    3. processing_owner,记录处理节点,也就是事件最终是由哪个节点处理的;通常由creating_owner自己处理,但也可能被收割,由其它节点处理。
    4. processing_state,当前的处理状态。
    5. error_count,处理错误计数,超过一定计数会被标记为处理失败。

    当前处理状态主要包括6种:

    1. AVAILABLE,表示待处理
    2. IN_PROCESSING,表示已经被dispatcher线程取走,正在处理中
    3. PROCESSED,表示已经处理
    4. REMOVED,表示已经被删除
    5. FAILED,表示处理失败
    6. REPEATED,表示被其它节点收割了

    除了bus_events待处理事件表,还有一个对应的bus-events-history事件历史记录表。不管成功还是失败,最终,事件会被写入历史记录表进行归档,作为事后审计或者人工干预的依据。

    上图下方给出了数据库表的github链接,你可以进一步参考学习。

    Killbill PersistentBus处理状态迁移

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KfBFSxeO-1593683238163)(images/states_transition.png)]

    上图给出了killbill PersistentBus的事件处理状态迁移图。

    1. 刚开始事件处于AVAILABLE待处理状态;
    2. 之后事件被dispatcher线程拉取,进入IN_PROCESSING处理中状态;
    3. 之后,如果事件处理器成功处理了事件,那么事件就进入PROCESSED已经处理状态;
    4. 如果事件处理器处理事件失败,那么事件的错误计数会被增加1,如果错误计数还没有超过最大失败重试阀值,那么事件就会重新进入AVAILABLE状态;
    5. 如果事件的错误数量超过了最大失败重试阀值,那么事件就会进入FAILED失败状态;
    6. 如果负责待处理事件的节点挂了,那么到达一定的时间间隔,对应的事件会被收割进入REAPED被收割状态。

    上图有一个通过API触发进入的REMOVED移除状态,这个是给通知队列用的,用户可以通过API移除对应的通知消息。顺便提一下,除了事件/消息队列,Killbill queue也是支持通知队列(或者说延迟消息队列)的。

    模式二:变更数据捕获(Change Data Capture, CDC)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clNWZb4s-1593683238164)(images/cdc.png)]

    对于事务性双写这个问题,业界沉淀下来比较实践的做法,其中第二种,就是所谓的变更数据捕获,英文称为Change Data Capture,简称CDC。

    变更数据捕获的原理也不复杂,它利用了数据库的事务日志记录。一般数据库,对于变更提交操作,都记录所谓事务日志Transaction Log,也称为提交日志Commit Log,比方说MySQL支持binlog,Postgres支持Write Ahead log。事务日志可以简单理解为数据库本地的一个文件队列,它记录了按时间顺序发生的对数据库表的变更提交记录。

    下面我们通过例子来展示这个变更数据捕获的流程,我们这里假定Order Service要添加一个新订单。

    第一步,Order Service将新订单记录写入Order表,并且提交。因为这是一次表变更操作,所以这次变更会被记录到数据库的事务日志当中,其中内容包括发生的变更数据。

    第二步,我们还需要引入一个称为Transaction Log Miner这样的角色,这个Miner负责订阅在事务日志队列上,如果有新的变更记录,Miner就会捕获到变更记录。

    然后第三步,Miner会将变更记录发送到MQ消息队列。同之前的Message Relay一样,这里的发送到MQ也是At Least Once语义,消息可能会被重复发送,所以MQ之后的消费者需要做去重或者幂等处理。

    总之,CDC技术同样可以保证,对Order表的修改,然后将对应事件发送到MQ,这两个动作可以实现事务性,也就是实现数据分发的事务性。

    注意,这里的CDC一般是一个独立部署的服务,生产中需要做好高可用部署,并且做好监控告警。否则如果CDC挂了,消息也就发不出来,然后,依赖于消息的各种消费方也将无法正常工作。

    CDC开源项目(企业级)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DCmTY4ui-1593683238164)(images/open_source_cdc.png)]

    当前,有几个比较成熟的企业级的CDC开源项目,我这边收集了一些,供大家学习参考:

    1. 第一个是阿里开源的Canal,目前在github上有超过1.4万颗星,这个项目在国内用得比较多,之前在拍拍贷的实时数据场景,Canal也有不少成功的应用。Canal主要支持MySQL binlog的增量订阅和消费。它是基于MySQL的Master/Slave机制,它的Miner角色是通过伪装成Slave来实现的。这个项目的使用文档相对比较完善,建议大家一步参考学习。
    2. 第二个是Redhat开源的Debezium,目前在github上有超过3.2k星,这个项目在国外用得较多。Debezium主要是在Kafka Connect的基础上开发的,它不仅支持mysql数据库,还支持postgres/sqlserver/mongodb等数据库。
    3. 第三个是Zendesk开源的Maxwell,目前在github上有超过2.1k星。Maxwell是一个轻量级的CDC Deamon,主要支持MySQL binlog的变更数据捕获和处理。
    4. 第四个是Airbnb开源的SpinalTap,目前在github上有两百多颗星。SpinalTap主要支持MySQL binlog的变更捕获和处理。这个项目的星虽然不多,但是它是在Airbnb SOA服务化过程中,通过实践落地出来的一个项目,值得参考。

    对于上面的这些项目,如果你想生产使用的话,波波推荐的是阿里的Canal,因为这个项目毕竟是国内大厂阿里落地出来,而且在国内已经有不少企业落地案例。其它几个项目,你也可以参考研究。

    学习参考 ~ Eventuate-Tram

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8a8y0Vl-1593683238165)(images/eventuate-tram.png)]

    既然谈到这个CDC,这里有必要提到一个人和一本书,这个人叫Chris Chardson,他是美国的老一辈的技术大牛,曾今是第一代的Cloud Foundry项目的创始人(后来Cloud Foundry被Pivotal所收购)。近几年,Chris Chardson开始转战微服务领域,这两年,他还专门写了一本书,叫《微服务设计模式》,英文名是《Microservices Patterns》。这本书主要是讲微服务架构和设计模式的,内容还不错,是我推荐大家阅读的。

    Charis Chardson还专门开发了一个叫Eventuate-Tram的开源项目(这个项目也有商业版),另外他的微服务书里头也详细介绍了这个项目。这个项目可以说是一个大集成框架,它不仅实现了DDD领域驱动开发模式,CQRS命令查询职责分离模式,事件溯源模式,还实现了Saga事务状态机模式。当然,这个项目的底层也实现了CDC变更数据捕获模式。

    波波认为,Charis的项目,作为学习研究还是有价值的,但是暂不建议生产级使用,因为他的东西不是一线企业落地出来的,主要是他个人开发的。至于说Charis的项目能否在一线企业落地,还有待时间的进一步检验。

    Transactional Outbox vs CDC

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YPEv6T5X-1593683238166)(images/outbox_vs_cdc.png)]

    好的,前面我介绍了解决数据的事务性分发的两种落地模式,一种是事务性发件箱模式,另外一种是变更数据捕获模式,这两种模式其实各有优劣,为了帮助大家做选型决策,我这边对这两种模式进行一个比较,请看上面的比较表格:

    1. 首先比较一下复杂性,事务性发件箱相对比较简单,简单做法只需要在数据库中增加一个发件箱表,然后再启一个Poller线程拉消息和发消息就可以了。CDC技术相对比较复杂,需要你深入理解数据库的事务日志格式和协议。另外Miner的实现也不简单,要保证不丢消息,如果生产部署的话,还要考虑Miner的高可用部署,还有监控告警等环节。
    2. 第二个比较的是Polling延迟和开销。事务性发件箱的Polling是近实时的,同时如果频繁拉数据库表,难免会有性能开销。CDC是比较实时的,同时它不侵入数据库和表,所以它的性能开销相对小。
    3. 第三个比较的是应用侵入性。事务性发件箱是有一定的应用侵入性的,应用在更新业务数据的同时,还要单独发送消息。CDC对应用是无侵入的,因为它拉取的是数据库事务日志,这个和应用是不直接耦合的。当然,CDC和事务性发件箱模式并不排斥,你可以在应用层采用事务性发件箱模式,同时仍然采用CDC到数据库去捕获和发件箱中的消息对应的事务日志。这个方法对应用有一定的侵入性,但是通过CDC可以获得较好的数据同步性能。
    4. 第四点是适用场合。事务性发件箱主要适用于中小规模的企业,因为做法比较简单,一个开发人员也可以搞定。CDC则主要适用于中大规模互联网企业,最好有独立框架团队负责CDC的治理和维护。像Netflix/Airbnb这样的一线互联网公司,也是在中后期才引入CDC技术的[参考附录1/2/3]。

    Single Source of Truth

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ShvO3vO-1593683238167)(images/single_source_of_truth.png)]

    前面我解答了如何解决微服务的数据一致性分发问题,也给出了可落地的方案。最后,我特别说明在实践中进行数据分发的一个原则,叫Single Source of Truth,翻成中文就是单一真实数据源。它的意思是说,你要实现数据分发,目标服务可以有很多,但是一定要注意,数据的主人只能有一个,它是数据的权威记录系统(canonical system of record),其它的数据都是只读的,非权威的拷贝(read-only, non-authoritative copy)。

    换句话说,任何时候,对于某类数据,它主人应该是唯一的,它是Single Source of Truth,只有它可以修改数据,其它的服务可以获得数据拷贝,做本地缓存也没问题,但是这些数据都是只读的,不能修改。

    只有遵循这条原则,数据分发才能正常工作,不会产生不一致的情况。

    结论

    1. Netflix和Airbnb等一线互联网公司的实践证明,企业要真正实现松散耦合、可扩展和高性能的微服务架构,那么底层的数据分发同步能力是非常关键的。
    2. 数据分发技术,简单的可以采用事务性发件箱模式来实现,重量级的可以考虑变更数据捕获CDC技术来实现。事务性发件箱可以参考Killbill Queue的实现,CDC可以参考阿里的Canal等开源产品来实现。
    3. 最简单的双写也是实现数据分发的一种方式,但是为了保证一致性,需要引入后台校验补偿程序。
    4. 最后,数据分发/同步的原则是:确保单一真实数据源(Single Source of Truth)。系统中数据的主人应该只有一个,只有主人可以写入数据,其它都是只读拷贝。

    课程推广

    最后,如果你对分布式系统设计感兴趣,那么我向你隆重推荐波波的新课《分布式系统案例课》,这门课程已经在极客时间上推出。

    通过这门课的学习,你将获得4点收获:

    1. 学习如何设计中大型系统
    2. 深入理解分布式核心技术
    3. 为架构师面试做准备
    4. 分享架构师成长指南

    本文内容也是新课程第四章的一部分。下面是新课程的宣传海报和详细大纲,欢迎关注!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l63GadHc-1593683238168)(images/promote.jpeg)]

    在这里插入图片描述

    附录

    1. Delta: A Data Synchronization and Enrichment Platform
    2. DBLog: A Generic Change-Data-Capture Framework
    3. Capturing Data Evolution in a Service Oriented Architecture
    展开全文
  • 原文:微服务下数据一致性的几种实现方式之概述 本人最近学习了一下微服务下数据一致性的特点,总结了目前的保障微服务下数据一致性的几种实现方式如下,以备后查。此篇文章旨在给大家一个基于微服务数据一致...
  • CAP,微服务数据一致性

    千次阅读 2016-10-10 13:22:47
    微服务是当下的热门话题,今天来聊下微服务中的一个敏感话题:如何保证微服务数据一致性。谈到分布式事务,就避免不了CAP理论。 CAP理论是指对于一个分布式计算系统来说,不可能同时满足以下三点:  1. ...
  • 众所周知,微服务架构解决了很多问题,通过分解复杂的单体式应用,在功能不变的情况,使应用被分解为多个可管理的服务,为采用单体式...比如服务运维变得更复杂,服务之间的依赖关系更复杂,数据一致性难以保证。...
  • Netflix/Airbnb等一线互联网公司的实践[参考附录1/2/3]表明,数据一致性分发能力,是构建松散耦合、可扩展和高性能的微服务架构的基础。 本文解释分布式微服务中的数据一致性分发问题,应用场景,并给出常见的解决...
  • 1、微服务架构数据一致性问题  电商为例,支付,订单,积分功能抽成三个服务,每个服务都有独立的数据库做数据存储。无论是修改订单状态失败,还是增加积分失败,都会导致数据的不一致 支付-------->订单-...
  • 目录分布式事务并不适用于微服务数据最终一致性(替代微服务中事务强一致性)可靠事件模式优点缺点案例 (支付宝转余额宝)为什么在进行可靠事件模式通过消息队列而不是RPC调用呢补偿模式 -sagas模型优点缺点案例(外卖) ...
  • 微服务架构下数据一致性的保证

    千人学习 2016-08-08 11:14:27
    传统应用使用本地事务和分布式事务保证数据一致性,但是在微服务架构中数据都是服务私有的,需要通过服务提供的api访问,分布式事务不再适用微服务架构。那么微服务架构又该怎么保证数据一致性呢?
  • 基于 Fescar 解决微服务架构下数据一致性的实践 Fescar 是一款开源的分布式事务解决方案,提供高性能和简单易用的分布式事务服务。 随着业务的快速发展,应用单体架构暴露出代码可维护性差,容错率低,测试难度大,...
  • 在《微服务架构数据一致性:概念及相关模式》中介绍了在微服务中实现数据一致性的三种方式,包括可靠事件模式、业务补偿模式、TCC模式。本文重点说一下可靠事件投递。可靠事件模式属于事件驱动架构,微服务完成...
  • 微服务系统架构,通过介绍可靠事件模式、补偿模式、TCC模式来实现数据的最终一致性
  • 摘要: 微服务 开源项目 Apache ServiceComb(incubating) 的 微服务事务的数据一致性解决方案 Saga[4](以下简称Saga)进行了演进。相对于上一版[2],新演进的设计主要有以下优势: 极大提升易用性。开发者只需使用2-...
  • 既然是微服务,就意味着面临大量的服务间的内部调用及服务依赖,这就意味着,如果一次请求的调用涉及到两个或多个微服务之间的调用,恰好有下游的微服务调用失败,我们就必须要考虑到回滚及服务间保证数据一致性的...
  • 3.微服务架构中应满足数据最终一致性原则。 4.微服务架构实现最终一致性的三种模式。 5.对账是最后的终极防线。 一、传统使用本地事务和分布式事务保证一致性 传统单机应用一般都会使用一个关系型数据库,好处是...
  • 微服务架构数据一致性:概念及相关模式 微服务架构数据一致性:概念及相关模式 微服务架构数据一致性:概念及相关模式 微服务架构数据一致性:概念及相关模式
  • 尤其是在微服务架构,每个微服务都有自己的数据库,导致微服务架构的系统不能简单地满足 ACID,我们就需要寻找微服务架构数据一致性解决方案。 传统情况,当一个事务要跨越多个分布式服务时,开发者想到的...
  • 设计到系统,其中绕不开的就是数据一致性,从本地事务,到后来的分布式事务,都能够有效的保证数据一致性。但是在微服务架构中,这两种方式都不是最好的选择。1. 使用本地事务和分布式事务保证一致性在传统的单击...
  • PAGE 18 微服务架构数据一致性方案 随着微服务架构的推广越来越多的公司采用微服务架构来构建自己的业务平台就像前边的文章说的微服务架构为业务开发带来了诸多好处的同时例如单一职责独立开发部署功能复用和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 60,471
精华内容 24,188
关键字:

微服务下的数据一致性