精华内容
下载资源
问答
  • 定义TPS:Transactions Per Second(每秒传输的事物处理个数),即服务器每秒处理的事务数。TPS包括一条消息入和一条消息出,加上一次用户数据库访问。(业务TPS = CAPS × 每个呼叫平均TPS)TPMC:Transactions Per ...

    定义

    TPS:Transactions Per Second(每秒传输的事物处理个数),即服务器每秒处理的事务数。TPS包括一条消息入和一条消息出,加上一次用户数据库访问。(业务TPS = CAPS × 每个呼叫平均TPS)

    TPMC:Transactions Per Minute(每分钟处理的交易量),tpmC值在国内外被广泛用于衡量计算机系统的事务处理能力。

    技术要点

    TPS是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。

    一般的,评价系统性能均以每秒钟完成的技术交易的数量来衡量。系统整体处理能力取决于处理能力最低模块的TPS值。

    衡量计算机系统性能的指标有很多种,其中与联机事务处理(OLTP)性能相关联的就是由TPC组织发布的TPC-C测试指标,其单位为tpmC,即每分钟处理的交易量(Transactions Per Minute)。

    TPC-C使用三种性能和价格度量,其中性能由TPC-C吞吐率衡量,单位是tpmC。tpm是transactions per minute的简称;C指TPC中的C基准程序。它的定义是每分钟内系统处理的新订单个数。服务器TPMC值计算根据TPC-C的标准,tpmC值是根据标准模型中New-Order事务的处理数目来计算的,一个New-Order事务由平均4-5个SQL语句处理完成,整个测试的执行过程中,New-Order处理占45%

    展开全文
  • 什么是事务? 本地事务 事务传播行为: 事务的四大特性 ACID 并发事务产生的问题可以分为4类 事务的隔离级别 什么是分布式事务 分布式涉及到的原理: CAP原理: BASE理论 柔性事务和刚性事务 柔性事务分为:...

    目录

    什么是事务?

    本地事务

    事务传播行为:

    事务的四大特性 ACID

    并发事务产生的问题可以分为4类

    事务的隔离级别

    什么是分布式事务

    分布式涉及到的原理:

    CAP原理:

    BASE理论

    柔性事务和刚性事务

    柔性事务分为:

    分布式一致性协议

    XA接口      

    Jta规范

    两阶段提交协议 2PC

    三阶段提交协议 3PC

    XA与TCC 的比较:

    分布式事务解决方案

    方案1:全局事务(DTP模型)

    分布式跨库事务:

    方案2 .TCC 两阶段补偿方案(Try -- Confirm/Cancel)

    方案3 .可靠消息最终一致性

    方案4 .最大努力通知方案:


     


    什么是事务?

    事务由一组指令操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作要么全都正确执行要么全都不要执行

    方案:

    悲观锁 for update

    乐观锁 version 字段版本控制

     

     @Autowired
        private TransactionTemplate txTemplate;
    
        public String sendOrderByTxTemplate(Order order) {
            Long orderId = order.getId();
            Boolean lockStatus = txTemplate.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus transactionStatus) {
                    Order order = new Order();
                    order.setId(orderId);
                    order.setOrderStatus("4");//处理中...
                    order.setVersion(0);
                    return 1 == orderMapper.updateByVersion(order);
                }
            });
    
            //j只有第一个进来的线程返回true
            //基于状态机的乐观锁..
            if (lockStatus) {//只允许第一个线程调用发货接口
                String flag = transService.invoke(url, orderId);//10s
                //只作用于代码块<===
                txTemplate.execute(new TransactionCallback<Boolean>() {
                    @Override
                    public Boolean doInTransaction(TransactionStatus transactionStatus) {
                        Order orderFin = new Order();
                        orderFin.setId(orderId);
                        orderFin.setOrderStatus(flag);//处理中...
                        orderMapper.update(orderFin);
                        return null;
                    }
                });
            }

    本地事务

    众所周知,数据库实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务

    事务传播行为:

    事务的四大特性 ACID

    我们首先看下一些书籍中的官方描述:

    数据库:

    原子性(Atomicity) 
    原子性要求: 事务是一个不可分割的执行单元,事务中的所有操作要么全都正确执行,要么全都不执行。

    一致性 (Consistency)
    一致性要求: 事务在开始前结束后,数据库的完整性约束没有被破坏

    隔离性 (Isolation)
    隔离性要求: 事务的执行是相互独立的,它们不会相互干扰,一个事务不会看另一个正在运行过程中的事务的数据

    持久性(Durability) 
    持久性要求: 一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失

    注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。

    --《Spring攻略》

    • 原子性(Atomictiy):事务是一个包含一系列操作的原子操作。事务的原子性确保这些操作全部完成或者全部失败。
    • 一致性(Consistency):一旦事务的所有操作结束,事务就被提交。然后你的数据和资源将处于遵循业务规则的一直状态。
    • 隔离性(Isolation):因为同时在相同数据集上可能有许多事务处理,每个事务应该与其他事务隔离,避免数据破坏。
    • 持久性(Durability):一旦事务完成,他的结果应该能够承受任何系统错误(想象一下在事务提交过程中机器的电源被切断的情况)。通常,事务的结果被写入持续性存储。

     

    --《企业应用架构模式》

    • 原子性(Atomictiy):在一个事务里,动作序列的每一个步骤都必须是要么全部成功,要么所有的工作都将回滚。部分完成不是一个事务的概念。
    • 一致性(Consistency):在事务开始和完成的时候,系统的资源都必须处于一致的、没有被破坏的状态。
    • 隔离性(Isolation):一个事务,直到它被成功提交之后,它的结果对于任何其他的事务才是可见的。
    • 持久性(Durability):一个已提交事务的任何结果都必须是永久性的,即“在任何系统崩溃的情况下都能保存下来”。

    那这四个属性,我们自己到底该如何理解呢。个人理解如下:
    如果给事务下一个定义:事务是一个有边界的指令操作序列,开始和结束都有明确的定义。

    1. 原子性
      举例1-比如现在有一个事务,包含3个sql语句(工作序列,或者是指令序列),sql-1,sql-2,sql-3,这3个sql语句,每一个在执行的时候,都是一个单元,这个单元的执行结果,有且仅有两种可能:成功和失败。
      举例2-再比如,我从账户1中转出1000人民币到账户2,当我从账户1中把钱转出来之后,系统就崩溃了。那么系统应该将我的账户状态置成我还没有转出钱之前的状态。
    2. 一致性
      举例1-有一个在线商务网站系统,有两张表,一张用户账户表(用户名、个人余额),一张商品库存表(商品ID,库存数量),用户花费30元购买1件商品,商品库存减1,账户余额减30.那么这样的结果就是一致的。否则,如果商品库存减1,账户余额没有变化,那么这样的结果就是不一致的。
      举例2-银行账户转账的例子,也一样,账户1中的钱减少1000,账户2中的钱就增加1000,这样的是一致的,否则不一致。
    3. 隔离性
      没有隔离性就没有一致性
      举例1-我们现在有两个事务方法,一个方法是查询数据的库存,一个是购买下单,那么这两个事务方法应该互不影响,不然会造成一系列的问题,个人一直认为,事务造成的下面这些问题是跟并发密不可分的,没有并发操作,单一的请求事务是不会有这样的问题的。

      并发事务产生的问题可以分为4类

      我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

      脏读  (我未提交你就读)
      事务1读到事务2尚未提交un-commit的事务中的数据。该数据可能会被回滚从而失效。 
      如果事务1拿着失效的数据去处理那就发生错误了。

      更新丢失  (我改你也改)
      当有两个并发执行的事务更新同一行数据,那么有可能一个事务会把另一个事务更新覆盖掉。 
      当数据库没有加任何锁操作的情况下会发生

      不可重复读 (我读中你捣乱[2])
      不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

      虚读:在事务1两次读取同一记录过程中事务2该记录进行修改操作,从而事务1第二次读到了不一样记录
      幻读事务1两次查询的过程中事务2该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化
       

      不可重复读 与 脏读 的区别? 
      脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。

      事务的隔离级别

      对事务的隔离性做一个详细的解释。

      在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间互不干扰。这确实能够完全保证数据的安全性实际业务系统中,这种方式性能不高因此,数据库定义了四种隔离级别隔离级别和数据库的性能呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差

      数据库的四种隔离级别
      数据库一共有如下四种隔离级别:

      从低到高

      Read uncommitted 读未提交  (①我写未完成时,你禁写可读; ②我读时,你可写可读)
      在该级别下,一个事务一行数据修改过程中不允许另一个事务该行数据进行修改操作但允许该行数据进行读操作。 
      因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读(虚读/幻读)问题。

      Read committed 读已提交  (①我写时未提交,你禁写读; 我读时,可写可读,[不管[爱咋咋地],想读写都可以])
      在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此出现不可重复读(虚读/幻读)的情况。

      Repeatable read 可重复读  (①我写时,你禁写读; ②我读时,你禁写可读) [MySQL默认隔离级别:可重复读]
      在该级别下,读事务禁止写事务允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读(虚读/幻读)),且写事务禁止其他一切事务

      Serializable 序列化(串行化)  (①我写时和②我读时,你禁止一切)
      该级别要求所有事务必须串行执行,因此能避免一切并发引起的问题,但效率很低

      隔离级别越高,越能保证数据完整性和一致性是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁乐观锁来控制。

      在《企业应用架构模式》一书中,有一句话描述就是:处理并发最主要的工具就是事务。

    4. 持久性
      对于数据库来讲,我的理解是这样,当我把sql-1、sql-2、sql-3提交之后,这个结果就一定会保存到数据库中,那如果提交-到写入这中间,突然断电,也没有关系,数据库服务器在重新启动之后一样会把数据写入磁盘,应该是通过日志的方式-仅个人理解。
      数据库一般都是通过事务日志的方式,write-ahead transaction log来保证持久性。write-ahead transaction log的意思是,事务中对数据库的改变在写入到数据库之前,首先写入到事务日志中。而事务日志是按照顺序排号的(LSN)。当数据库崩溃或者服务器断点时,重启动数据库,首先会检查日志顺序号,将本应对数据库做更改而未做的部分持久化到数据库,从而保证了持久性.

    什么是分布式事务

    • 分布式事务就是指事务的资源分别位于不同的分布式系统的不同节点之上的事务
    • 上面所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现跨数据库事务支持,这也就是大家常说的“分布式事务”。
    • 分布式事务涉及到操作多个数据库的事务,分布式事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点上。一个分布式事务可以看作是由多个分布式的操作序列组成的,通常可以把这一系列分布式的操作序列称为子事务,但由于在分布式事务中,各个子事务的执行是分布式的,因此要实现一种能够保证 ACID 特性的分布式事务处理系统就显得格外复杂。

    分布式事务产生的原因

    • 数据库分库分表

    ​    在单库单表场景下,当业务数据量达到单库单表的极限时,就需要考虑分库分表,将之前的单库单表拆分成多库多表;分库分表之后,原来在单个数据库上的事务操作,可能就变成跨多个数据库的操作,此时就需要使用分布式事务。

    • 业务服务化

    ​    业务服务化即业务按照面向服务(SOA)的架构拆分整个网站系统,所有的业务操作都以服务的方式对外发布,跨应用、跨服务的操作需要使用分布式事务才能保证数据的一致性。

     

    这里举一个分布式事务的典型例子——用户下单过程。 
    当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统.仓储服务等。整个下单的过程如下:

    用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
    此时订单系统会生成一条订单
    订单创建成功后,支付系统提供支付功能
    当支付完成后,由积分系统为该用户增加积分
    上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。

    分布式涉及到的原理:

    CAP原理:

      

     由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系统的CAP原理包含如下三个元素:

    概述
      分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。

    Consistency 一致性
      一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。

    Availability 可用性
      可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

    Parttition Tolerance 分区容忍性
      网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。

    权衡
      在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
    为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
    为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
    通常采取的策略是保证可用性,牺牲部分一致性,只确保最终一致性。

           当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间。

      

    BASE理论


            BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。

    1、基本可用  BA:(Basically Available ):

           指分布式系统在出现故障的时候,允许损失部分可用性保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。简单来说就是基本可用

    2、软状态  S:( Soft State):

            软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。简单来说就是状态可以在一段时间内不同步

    3、最终一致性  E:(Eventually Consistent ):

           系统中的所有数据副本经过一定时间后,最终能够达到一致的状态不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。简单来说就是在一定的时间窗口内, 最终数据达成一致即可。

    柔性事务和刚性事务

     

    1. 刚性事务满足ACID理论
    2. 柔性事务满足BASE理论(基本可用,最终一致)

    柔性事务分为:

    两阶段型 2PC(two phase commitment)
    补偿型TCC (try confirn/caclen
    异步确保型 (通过信息中间件)
    最大努力通知型。
     

    酸碱平衡
    ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。

    分布式一致性协议


    XA接口
          

    XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调事务管理器控制着全局事务,管理事务生命周期,并协调资源资源管理器负责控制和管理实际资源(如数据库或JMS队列)

    Jta规范


           作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:

    1.J2EE容器所提供的JTA实现(JBoss)
    2.独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里
    用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。

    两阶段提交协议 2PC

    两阶段提交协议 2PC
    分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

    该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
    所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失
    所有节点不会永久性损坏,即使损坏后仍然可以恢复


    1. 第一阶段(投票(准备)阶段)

    协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
    参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
    各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
    2. 第二阶段(提交执行阶段)

    当协调者节点从所有参与者节点获得的相应消息都为”同意”时:

    协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
    参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
    参与者节点向协调者节点发送”完成”消息。
    协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
    如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

    协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
    参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
    参与者节点向协调者节点发送”回滚完成”消息。
    协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
    不管最后结果如何,第二阶段都会结束当前事务。

    二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

    执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
    参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
    协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)
    二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
    为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。

    三阶段提交协议 3PC


    与两阶段提交不同的是,三阶段提交有两个改动点。

    引入超时机制。同时在协调者参与者中都引入超时机制
    在第一阶段和第二阶段中插入一个准备阶段保证了在最后提交阶段之前各参与节点状态一致的。
    也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

    1. CanCommit阶段

    3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

    事务询问 
    协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
    响应反馈 
    参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
    2. PreCommit阶段

    协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。 
    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

    发送预提交请求 
    协调者向参与者发送PreCommit请求,并进入Prepared阶段。

    事务预提交 
    参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

    响应反馈 
    如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

    假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

    发送中断请求 
    协调者向所有参与者发送abort请求。

    中断事务 
    参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

    3. doCommit阶段 
    该阶段进行真正的事务提交,也可以分为以下两种情况。

    • 3.1 执行提交

    发送提交请求 
    协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    事务提交 
    参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    响应反馈 
    事务提交完之后,向协调者发送Ack响应。
    完成事务 
    协调者接收到所有参与者的ack响应之后,完成事务。

    • 3.2 中断事务 

    协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

    发送中断请求 
    协调者向所有参与者发送abort请求

    事务回滚 
    参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

    反馈结果 
    参与者完成事务回滚之后,向协调者发送ACK消息

    中断事务 
    协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

     

    2PC与3PC提交区别
    增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大。
    三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。

     

    XA与TCC 的比较:

     

    分布式事务解决方案

     

    方案1:全局事务(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具体的实现是由数据库厂商来完成的。
    有没有基于DTP模型的分布式事务中间件?

    参考文献
    大规模SOA系统中的分布事务处理_程立
    Life beyond Distributed Transactions: an Apostate’s Opinion
    关于如何实现一个TCC分布式事务框架的一点思考
    How can a requestor ensure a consistent outcome across multiple, independent providers
    关于分布式事务、两阶段提交协议、三阶提交协议
    Three-phase commit protocol

    分布式跨库事务:

    pom依赖:

    DataSource:

    事务过程:

    对atomikos的最终提交commit的模拟过程:

     

    方案2 .TCC 两阶段补偿方案(Try -- Confirm/Cancel)


     

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

     

    1 预留资源阶段

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

    2 确认资源阶段

    • Confirm:执行业务 

    这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,

    而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。

    • Cancel:取消执行的业务 

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

    TCC原理:

     

    案例:

    优缺点及使用场景:

    TCC框架:

    TCC全局事务必须基于RM本地事务来实现全局事务
    TCC服务是由Try -- Confirm/Cancel业务构成的, 
    其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操作,必须要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。

     

    再考虑一下如下场景:A==>B转账,余额系统和红包系统是两个独立的系统

    假设服务B没有基于RM本地事务(以RDBS为例,可通过设置auto-commit为true来模拟),那么一旦[B:Try]操作中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则需要判断[B:Try]中哪些操作已经写到DB、哪些操作还没有写到DB:假设[B:Try]业务有5个写库操作,[B:Cancel]业务则需要逐个判断这5个操作是否生效,并将生效的操作执行反向操作。

    不幸的是,由于[B:Cancel]业务也有n(0<=n<=5)个反向的写库操作,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。因为,相比第一次[B:Cancel]操作,后续的[B:Cancel]操作还需要判断先前的[B:Cancel]操作的n(0<=n<=5)个写库中哪几个已经执行、哪几个还没有执行,这就涉及到了幂等性问题。而对幂等性的保障,又很可能还需要涉及额外的写库操作,该写库操作又会因为没有RM本地事务的支持而存在类似问题。。。可想而知,如果不基于RM本地事务,TCC事务框架是无法有效的管理TCC全局事务的。

    反之,基于RM本地事务的TCC事务,这种情况则会很容易处理:[B:Try]操作中途执行失败,TCC事务框架将其参与RM本地事务直接rollback即可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操作涉及的RM本地事务已经rollback”的情况下,根本无需执行[B:Cancel]操作。

    换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不需要考虑部分执行的情况。

    TCC事务框架应该提供Confirm/Cancel服务的幂等性保障
    一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用

    在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多。比如,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。因此,故障恢复机制后续仍然会重新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。

    既然Confirm/Cancel业务可能会被多次调用,就需要保障其幂等性。 
    那么,应该由TCC事务框架来提供幂等性保障?还是应该由业务系统自行来保障幂等性呢? 
    个人认为,应该是由TCC事务框架来提供幂等性保障。如果仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是可以的;然而,这是一类公共问题,毫无疑问,所有TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;而且,考虑一下由业务系统来负责幂等性需要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。

     

    方案3 .可靠消息最终一致性

    1 . 普通消息队列中间件

    2 . 基于RabbitMQ/RocketMQ消息中间件

    这种实现分布式事务的方式需要通过消息中间件来实现。假设有A和B两个系统,分别可以处理任务A和任务B。此时系统A中存在一个业务流程,需要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。

    在系统A处理任务A前,首先向消息中间件发送一条消息
    消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。
    消息中间件持久化成功后,便向系统A返回一个确认应答;
    系统A收到确认应答后,则可以开始处理任务A;
    任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。 
    但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。
    消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;
    当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。
    上述过程可以得出如下几个结论: 
    1. 消息中间件扮演者分布式事务协调者的角色。 
    2. 系统A完成任务A后,到任务B执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足BASE理论。

    上述过程中,如果任务A处理失败,那么需要进入回滚流程,如下图所示: 


    若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。和发送Commit请求一样,系统A发完之后便可以认为回滚已经完成,它便可以去做其他的事情。
    消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统B,从而不会触发系统B的任务B。
    此时系统又处于一致性状态,因为任务A和任务B都没有执行。

    上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。

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

    提交 
    若获得的状态是“提交”,则将该消息投递给系统B。
    回滚 
    若获得的状态是“回滚”,则直接将条消息丢弃。
    处理中 
    若获得的状态是“处理中”,则继续等待。


    消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况,而且能降低上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后便可以处理其他任务,无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补,这样大大降低上游系统的阻塞时间,提升系统的并发度。

    下面来说一说消息投递过程的可靠性保证。 
    当上游系统执行完任务并向消息中间件提交了Commit指令后,便可以处理其他任务了,此时它可以认为事务已经完成,接下来消息中间件一定会保证消息被下游系统成功消费掉!那么这是怎么做到的呢?这由消息中间件的投递流程来保证。

    消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕!

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


    有的同学可能要问:消息投递失败后为什么不回滚消息,而是不断尝试重新投递

    这就涉及到整套分布式事务系统的实现成本问题。 
    我们知道,当系统A将向消息中间件发送Commit指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统A事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本。

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

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

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

    异步提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。 
    我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的BASE理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。

    方案4 .最大努力通知方案:

    最大努力通知也被称为定期校对,其实在方案二中已经包含,这里再单独介绍,主要是为了知识体系的完整性。这种方案也需要消息中间件的参与,其过程如下:

    上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,然后上游系统可以去做别的事情了;
    消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;
    当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。
    上面是一个理想化的过程,但在实际场景中,往往会出现如下几种意外情况:

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

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

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

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

    因此,尽量选择支持事务型消息的消息中间件来实现分布式事务,如RocketMQ/RabbitMQ。

    COOKIE方面:

    展开全文
  • 用于IoTeX区块链交易和gRPC API的Protobuf和实用程序包 \proto包括IoTeX区块链使用的所有核心数据对象和gRPC API的protobuf定义 \golang包含为go语言生成的protobuf文件 入门 正在安装 安装协议 从安装Google协议...
  • 在分布式系统中一次操作需要由多个服务协同完成,这种由不同的服务之间通过网络协同完成的事务称为分布式事务。本文详解介绍七种常见分布式事务的原理以及优缺点和适用场景(2PC、3PC、TCC、Saga、本地事务表、MQ...

    分布式事务:在分布式系统中一次操作需要由多个服务协同完成,这种由不同的服务之间通过网络协同完成的事务称为分布式事务

    一、2PC:

            2PC,两阶段提交,将事务的提交过程分为资源准备和资源提交两个阶段,并且由事务协调者来协调所有事务参与者,如果准备阶段所有事务参与者都预留资源成功,则进行第二阶段的资源提交,否则事务协调者回滚资源。

    1、第一阶段:准备阶段

    由事务协调者询问通知各个事务参与者,是否准备好了执行事务,具体流程图如下:

    • ① 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
    • ② 各参与者执行本地事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
    • ③ 如参与者执行成功,给协调者反馈同意,否则反馈中止,表示事务不可以执行

    2、第二阶段:提交阶段

            协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者commit提交或者rollback回滚

    (1)事务提交:

            当第一阶段所有参与者都反馈同意时,协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,具体流程如下:

    • ① 协调者节点向所有参与者节点发出正式提交的 commit 请求。
    • ② 收到协调者的 commit 请求后,参与者正式执行事务提交操作,并释放在整个事务期间内占用的资源。
    • ③ 参与者完成事务提交后,向协调者节点发送ACK消息。
    • ④ 协调者节点收到所有参与者节点反馈的ACK消息后,完成事务。

    所以,正常提交时,事务的完整流程图如下:

     (2)事务回滚:

    如果任意一个参与者节点在第一阶段返回的消息为中止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,具体流程如下:

    • ① 协调者向所有参与者发出 rollback 回滚操作的请求
    • ② 参与者利用阶段一写入的undo信息执行回滚,并释放在整个事务期间内占用的资源
    • ③ 参与者在完成事务回滚之后,向协调者发送回滚完成的ACK消息
    • ④ 协调者收到所有参与者反馈的ACK消息后,取消事务

    所以,事务回滚时,完整流程图如下:

     3、2PC的缺点:

    二阶段提交确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

    (1)性能问题:执行过程中,所有参与节点都是事务阻塞性的,当参与者占有公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态,为了数据的一致性而牺牲了可用性,对性能影响较大,不适合高并发高性能场景

    (2)可靠性问题:2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

    (3)数据一致性问题:在阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

    (4)二阶段无法解决的问题:协调者在发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了,那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

    二、3PC:

            3PC,三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点:

    • (1)在协调者和参与者中都引入超时机制
    • (2)在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

            所以3PC会分为3个阶段,CanCommit 准备阶段、PreCommit 预提交阶段、DoCommit 提交阶段,处理流程如下:

     1、阶段一:CanCommit 准备阶段

            协调者向参与者发送 canCommit 请求,参与者如果可以提交就返回Yes响应,否则返回No响应,具体流程如下:

    • (1)事务询问:协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
    • (2)响应反馈:参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。

    2、阶段二:PreCommit 阶段

            协调者根据参与者的反应情况来决定是否可以进行事务的 PreCommit 操作。根据响应情况,有以下两种可能:

    (1)执行事务:

    假如所有参与者均反馈 yes,协调者预执行事务,具体如下:

    • ① 发送预提交请求:协调者向参与者发送 PreCommit 请求,并进入准备阶段
    • ② 事务预提交 :参与者接收到 PreCommit 请求后,会执行本地事务操作,并将 undo 和 redo 信息记录到事务日志中(但不提交事务)
    • ③ 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

     (2)中断事务:

    假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断,流程如下:

    • ① 发送中断请求 :协调者向所有参与者发送 abort 请求。
    • ② 中断事务 :参与者收到来自协调者的 abort 请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

    3、阶段三:doCommit阶段

    该阶段进行真正的事务提交,也可以分为以下两种情况:

    (1)提交事务:

    • ① 发送提交请求:协调接收到所有参与者发送的ACK响应,那么他将从预提交状态进入到提交状态,并向所有参与者发送 doCommit 请求
    • ② 本地事务提交:参与者接收到doCommit请求之后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源
    • ③ 响应反馈:事务提交完之后,向协调者发送ack响应。
    • ④ 完成事务:协调者接收到所有参与者的ack响应之后,完成事务。

    (2)中断事务:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务

    • ① 发送中断请求:如果协调者处于工作状态,向所有参与者发出 abort 请求
    • ② 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    • ③ 反馈结果:参与者完成事务回滚之后,向协调者反馈ACK消息
    • ④ 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

            进入doCommit阶段后,无论协调者出现问题,或者协调者与参与者之间的网络出现问题,都会导致参与者无法接收到协调者发出的 doCommit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。这其实基于概率来决定的,当进入第三阶段时,说明第一阶段收到所有参与者的CanCommit响应都是Yes,意味着大家都同意修改了,并且第二阶段所有的参与者对协调者的PreCommit请求也都是同意的。所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。

    4、3PC的优缺点:

            与2PC相比,3PC降低了阻塞范围,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务。

            数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者因为网络问题无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

    2PC和3PC都无法保证数据绝对的一致性,一般为了预防这种问题,可以添加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息。

    三、TCC:

    1、什么是TCC:

            TCC(Try Confirm Cancel)是应用层的两阶段提交,所以对代码的侵入性强,其核心思想是:针对每个操作,都要实现对应的确认和补偿操作,也就是业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作,第一阶段由业务代码编排来调用Try接口进行资源预留,当所有参与者的 Try 接口都成功了,事务协调者提交事务,并调用参与者的 confirm 接口真正提交业务操作,否则调用每个参与者的 cancel 接口回滚事务,并且由于 confirm 或者 cancel 有可能会重试,因此对应的部分需要支持幂等。

    2、TCC的执行流程:

            TCC的执行流程可以分为两个阶段,分别如下:

    (1)第一阶段:Try,业务系统做检测并预留资源 (加锁,锁住资源),比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库存给锁定住。

    (2)第二阶段:根据第一阶段的结果决定是执行confirm还是cancel

    • Confirm:执行真正的业务(执行业务,释放锁)
    • Cancle:是对Try阶段预留资源的释放(出问题,释放锁)

     3、TCC如何保证最终一致性:

    • TCC 事务机制以 Try 为中心的,Confirm 确认操作和 Cancel 取消操作都是围绕 Try 而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有 Cancel 取消操作可以将其执行结果撤销。
    • Try阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的,也就是说只要 Try 成功,Confirm 一定成功(TCC设计之初的定义)
    • Confirm 与 Cancel 如果失败,由TCC框架进行重试补偿
    • 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入

    4、TCC的注意事项:

    (1)允许空回滚:

            空回滚出现的原因是 Try 超时或者丢包,导致 TCC 分布式事务二阶段的 回滚,触发 Cancel 操作,此时事务参与者未收到Try,但是却收到了Cancel 请求,如下图所示:

            所以 cancel 接口在实现时需要允许空回滚,也就是 Cancel 执行时如果发现没有对应的事务 xid 或主键时,需要返回回滚成功,让事务服务管理器认为已回滚。

    (2)防悬挂控制:

            悬挂指的是二阶段的 Cancel 比 一阶段的Try 操作先执行,出现该问题的原因是 Try 由于网络拥堵而超时,导致事务管理器生成回滚,触发 Cancel 接口,但之后拥堵在网络的 Try 操作又被资源管理器收到了,但是 Cancel 比 Try 先到。但按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,所以此时应该拒绝执行空回滚之后到来的 Try 操作,否则会产生数据不一致。因此我们可以在 Cancel 空回滚返回成功之前,先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口执行前先检查这条事务xid或业务主键是否已经标记为回滚成功,如果是则不执行 Try 的业务操作。

     (3)幂等控制:

            由于网络原因或者重试操作都有可能导致 Try - Confirm - Cancel 3个操作的重复执行,所以使用 TCC 时需要注意这三个操作的幂等控制,通常我们可以使用事务 xid 或业务主键判重来控制。

    5、TCC方案的优缺点:

    (1)TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:

    • 性能提升:具体业务来实现,控制资源锁的粒度变小,不会锁定整个资源。
    • 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
    • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

    (2)缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

    四、Saga事务:

    1、什么是Saga事务:

            Saga 事务核心思想是将长事务拆分为多个本地短事务并依次正常提交,如果所有短事务均执行成功,那么分布式事务提交;如果出现某个参与者执行本地事务失败,则由 Saga 事务协调器协调根据相反顺序调用补偿操作,回滚已提交的参与者,使分布式事务回到最初始的状态。Saga 事务基本协议如下:

    • (1)每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
    • (2)每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

            与TCC事务补偿机制相比,TCC有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;Saga事务没有预留动作,直接提交。

    2、Saga的恢复策略:

    对于事务异常,Saga提供了两种恢复策略,分别如下:

    (1)向后恢复(backward recovery):

    当执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式,这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。如下图:

             从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿,对应的执行顺序为:T1,T2,T3,C3,C2,C1。

    (2)向前恢复(forward recovery):

            对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功,这种方式适用于必须要成功的场景,事务失败了重试,不需要补偿。流程如下图:

     3、Saga事务的实现方式:

    Saga事务有两种不同的实现方式,分别如下:

    • 命令协调(Order Orchestrator)
    • 事件编排(Event Choreographyo)

    (1)命令协调:

            中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图:

    • ① 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
    • ② OSO 向库存服务请求扣减库存,库存服务回复处理结果。
    • ③ OSO 向订单服务请求创建订单,订单服务回复创建结果。
    • ④ OSO 向支付服务请求支付,支付服务回复处理结果。
    • ⑤ 主业务逻辑接收并处理 OSO 事务处理结果回复。

            中央协调器 OSO 必须事先知道执行整个事务所需的流程,如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚,基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

    (2)事件编排:

            命令协调方式基于中央协调器实现,所以有单点风险,但是事件编排方式没有中央协调器。事件编排的实现方式中,每个服务产生自己的时间并监听其他服务的事件来决定是否应采取行动。

            在事件编排方法中,第一个服务执行一个事务,然后发布一个事件,该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

    • ① 事务发起方的主业务逻辑发布开始订单事件。
    • ② 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
    • ③ 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
    • ④ 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
    • ⑤ 主业务逻辑监听订单已支付事件并处理。

            如果事务涉及 2 至 4 个步骤,则非常合适使用事件编排方式,它是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。

    4、Saga事务的优缺点:

    (1)命令协调设计的优缺点:

    ① 优点:

    • 服务之间关系简单,避免服务间循环依赖,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
    • 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
    • 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。

    ② 缺点:

    • 中央协调器处理逻辑容易变得庞大复杂,导致难以维护。
    • 存在协调器单点故障风险。

    (2)事件编排设计的优缺点:

    ① 优点:

    • 避免中央协调器单点故障风险。
    • 当涉及的步骤较少服务开发简单,容易实现。

    ② 缺点:

    • 服务之间存在循环依赖的风险。
    • 当涉及的步骤较多,服务间关系混乱,难以追踪调测。

            由于 Saga 模型没有 Prepare 阶段,因此事务间不能保证隔离性。当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。

    五、本地消息表:

    1、什么是本地消息表:

            本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理,在该方案中主要有两种角色:事务主动方和事务被动方。事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

            这样可以避免以下两种情况导致的数据不一致性:

    • 业务处理成功、事务消息发送失败
    • 业务处理失败、事务消息发送成功

    2、本地消息表的执行流程:

    • ① 事务主动方在同一个本地事务中处理业务和写消息表操作
    • ② 事务主动方通过消息中间件,通知事务被动方处理事务消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
    • ③ 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
    • ④ 事务主动方接收中间件的消息,更新消息表的状态为已处理。

    一些必要的容错处理如下:

    • 当①处理出错,由于还在事务主动方的本地事务中,直接回滚即可
    • 当②、③处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,通知事务被动方重新读取消息处理业务即可。
    • 如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
    • 如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。

    3、本地消息表的优缺点:

    (1)优点:

    • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
    • 方案轻量,容易实现。

    (2)缺点:

    • 与具体的业务场景绑定,耦合性强,不可公用
    • 消息数据与业务数据同库,占用业务系统资源
    • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限

    六、MQ事务消息:

    1、MQ事务消息的执行流程:

            基于MQ的分布式事务方案本质上是对本地消息表的封装,整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了MQ内部,而不是业务数据库中,如下图:

             由于将本地消息表存在了MQ内部,那么MQ内部的处理尤为重要,下面主要基于 RocketMQ4.3 之后的版本介绍 MQ 的分布式事务方案

    2、RocketMQ事务消息:

            在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,而 RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:

     (1)正常情况:

    在事务主动方服务正常,没有发生故障的情况下,发消息流程如下:

    • 步骤①:发送方向 MQ Server(MQ服务方)发送 half 消息
    • 步骤②:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功
    • 步骤③:发送方开始执行本地事务逻辑
    • 步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
    • 最终步骤:MQ Server 如果收到的是 commit 操作,则将半消息标记为可投递,MQ订阅方最终将收到该消息;若收到的是 rollback 操作则删除 half 半消息,订阅方将不会接受该消息

    (2)异常情况:

            在断网或者应用重启等异常情况下,图中的步骤④提交的二次确认超时未到达 MQ Server,此时的处理逻辑如下:

    • 步骤⑤:MQ Server 对该消息发起消息回查
    • 步骤⑥:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
    • 步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
    • 最终步骤:MQ Server基于 commit/rollback 对消息进行投递或者删除。

    3、MQ事务消息的优缺点:

    (1)优点:相比本地消息表方案,MQ 事务方案优点是:

    • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合
    • 吞吐量大于使用本地消息表方案

    (2)缺点:

    • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
    • 业务处理服务需要实现消息状态回查接口。

    七、最大努力通知:

            最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到主动方发送的消息,此时可以调用事务主动方提供的消息校对的接口主动获取

             在可靠消息事务中,事务主动方需要将消息发送出去,并且让接收方成功接收消息,这种可靠性发送是由事务主动方保证的;但是最大努力通知,事务主动方仅仅是尽最大努力(重试,轮询....)将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。

            所以最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。

    八、各方案常见使用场景总结:

    • 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
    • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
    • 本地消息表/MQ 事务:适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
    • Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga 事务较适用于补偿动作容易处理的场景

    参考文章:https://blog.csdn.net/qq_34162294/article/details/120984951

    展开全文
  • 1、事务消费介绍 我们经常支付宝转账余额宝,这是日常生活的一件普通小事,但是我们思考支付宝扣除转账的钱之后,如果系统挂掉怎么办,这时余额宝账户并没有增加相应的金额,数据就会出现不一致状况了。 上述场景...

    1、事务消费介绍

    我们经常支付宝转账余额宝,这是日常生活的一件普通小事,但是我们思考支付宝扣除转账的钱之后,如果系统挂掉怎么办,这时余额宝账户并没有增加相应的金额,数据就会出现不一致状况了。

    上述场景在各个类型的系统中都能找到相似影子,比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?!在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增加一条记录外,还得去商家账户表中找到这个商家并扣除广告费吧,怎么保证?!等等,相信大家或多或多少都能碰到相似情景。

    本质上问题可以抽象为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。

    如果是单机系统(数据库实例也在同一个系统上)的话,我们可以用本地事务轻松解决:

    还是以支付宝转账余额宝为例(比如转账10000块钱),假设有

    支付宝账户表:A(id,userId,amount)
    余额宝账户表:B(id,userId,amount)
    用户的userId=1;

    从支付宝转账1万块钱到余额宝的动作分为两步:

    1)支付宝表扣除1万:update A set amount=amount-10000 where userId=1;
    2)余额宝表增加1万:update B set amount=amount+10000 where userId=1;

    如何确保支付宝余额宝收支平衡呢?

    有人说这个很简单嘛,可以用事务解决。

    Begin transaction 
      update A set amount=amount-10000 where userId=1;
     update B set amount=amount+10000 where userId=1;
    End transaction 
    commit;
    

    这样确实能解决,如果你使用spring的话一个注解就能搞定上述事务功能。

    @Transactional(rollbackFor=Exception.class) 
    public void update() { 
          //更新A表 
          updateATable();
          //更新B表
          updateBTable();
    }
    

    如果系统规模较小,数据表都在一个数据库实例上,上述本地事务方式可以很好地运行,但是如果系统规模较大,比如支付宝账户表和余额宝账户表显然不会在同一个数据库实例上,他们往往分布在不同的物理节点上,这时本地事务已经失去用武之地。

    下面我们来看看比较主流的两种方案:

    2、分布式事务—————— 两阶段提交协议

    两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器TC和若干事务执行者两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。

    我们根据上面的图来看看主要流程:

    1) 我们的应用程序(client)发起一个开始请求到TC(transaction);

    2) TC先将prepare消息写到本地日志,之后向所有的Si发起prepare消息。以支付宝转账到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应账目扣款1万,TC给B的prepare消息是通知余额宝数据库相应账目增加1w。为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证的效果,如果没有本地日志(凭证),出问题容易死无对证;

    3) Si收到prepare消息后,执行具体本机事务,但不会进行commit,如果成功返回yes,不成功返回no。同理,返回前都应把要返回的消息写到日志里,当作凭证。

    4) TC收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。

    注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一Si从故障中恢复后,先检查本机的日志,如果已收到commit,则提交,如果abort则回滚。如果是yes,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在prepare阶段Si就崩溃了,因此需要回滚。

    现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/),来快速实现。)

    不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?

    1)两阶段提交涉及多次节点间的网络通信,通信时间太长!
    2)事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!

    正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过其他途径来解决数据一致性问题。

    3、使用消息队列来避免分布式事务

    如果仔细观察生活的话,生活的很多场景已经给了我们提示。

    比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。

    还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。

    那么我们如何可靠保存凭证(消息)有两种方法:

    1)业务与消息耦合的方式

    支付宝在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。

    Begin transaction 
           update A set amount=amount-10000 where userId=1; 
           insert into message(userId, amount,status) values(1, 10000, 1); 
    End transaction 
    commit;
    

    上述事务能保证只要支付宝账户里被扣了钱,消息一定能保存下来。

    当上述事务提交成功后,我们通过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。

    2)业务与消息解耦方式

    上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看不够优雅,而且容易诱发其他问题。为了解耦,可以采用以下方式。

    a)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;

    b)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;

    c)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;

    d)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。

    优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
    缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。

    4、那么如何解决消息重复投递的问题?

    还有一个很严重的问题就是消息重复投递,以我们支付宝转账到余额宝为例,如果相同的消息被重复投递两次,那么我们余额宝账户将会增加2万而不是1万了(上面讲顺序消费是讲过,这里再提一下)。

    为什么相同的消息会被重复投递?比如余额宝处理完消息msg后,发送了处理成功的消息给支付宝,正常情况下支付宝应该要删除消息msg,但如果支付宝这时候悲剧的挂了,重启后一看消息msg还在,就会继续发送消息msg。

    解决方法很简单,在余额宝这边增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)

    For each msg in queue 
    	Begin transaction 
    	  select count(*) as cnt from message_apply where msg_id=msg.msg_id; 
    	  if cnt==0 then 
    		update B set amount=amount+10000 where userId=1; 
    		insert into message_apply(msg_id) values(msg.msg_id); 
    	  end if
    	End transaction 
    	commit;
    End For
    

    为了方便大家理解,我们再来举一个银行转账的示例(和上一个例子差不多):

    比如,Bob向Smith转账100块。

    在单机环境下,执行事务的情况,大概是下面这个样子:

    当用户增长到一定程度,Bob和Smith的账户及余额信息已经不在同一台服务器上了,那么上面的流程就变成了这样:

    这时候你会发现,同样是一个转账的业务,在集群环境下,耗时居然成倍的增长,这显然是不能够接受的。那如何来规避这个问题?

    5、大事务 = 小事务 + 异步

    将大事务拆分成多个小事务异步执行。这样基本上能够将跨机事务的执行效率优化到与单机一致。转账的事务就可以分解成如下两个小事务:

    图中执行本地事务(Bob账户扣款)和发送异步消息应该保证同时成功或者同时失败,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢?

    首先看下先发送消息的情况,大致的示意图如下:

    存在的问题是:如果消息发送成功,但是扣款失败,消费端就会消费此消息,进而向Smith账户加钱。

    先发消息不行,那就先扣款吧,大致的示意图如下:

    存在的问题跟上面类似:如果扣款成功,发送消息失败,就会出现Bob扣钱了,但是Smith账户未加钱。

    可能大家会有很多的方法来解决这个问题,比如:直接将发消息放到Bob扣款的事务中去,如果发送失败,抛出异常,事务回滚。这样的处理方式也符合“恰好”不需要解决的原则。

    RocketMQ支持事务消息,下面来看看RocketMQ是怎样来实现的?

    RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。

    细心的你可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?

    RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

    6、Rocket事务流程处理分析

    那我们来看下RocketMQ源码,是如何处理事务消息的。

    客户端发送事务消息的部分(完整代码请查看:rocketmq-example工程下的com.alibaba.rocketmq.example.transaction.TransactionProducer)

    // =============================发送事务消息的一系列准备工作========================================
    // 未决事务,MQ服务器回查客户端
    // 也就是上文所说的,当RocketMQ发现`Prepared消息`时,会根据这个Listener实现的策略来决断事务
    TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
    // 构造事务消息的生产者
    TransactionMQProducer producer = new TransactionMQProducer("groupName");
    // 设置事务决断处理类
    producer.setTransactionCheckListener(transactionCheckListener);
    // 本地事务的处理逻辑,相当于示例中检查Bob账户并扣钱的逻辑
    TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
    producer.start()
    // 构造MSG,省略构造参数
    Message msg = new Message(......);
    // 发送消息
    SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);
    producer.shutdown();

    接着查看sendMessageInTransaction方法的源码,总共分为3个阶段:发送Prepared消息、执行本地事务、发送确认消息。

    //  ================================事务消息的发送过程=============================================
    public TransactionSendResult sendMessageInTransaction(.....) {
        // 逻辑代码,非实际代码
        // 1.发送消息
        sendResult = this.send(msg);
        // sendResult.getSendStatus() == SEND_OK
        // 2.如果消息发送成功,处理与消息关联的本地事务单元
        LocalTransactionState localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
        // 3.结束事务
        this.endTransaction(sendResult, localTransactionState, localException);
    }
    
    

    endTransaction方法会将请求发往broker(mq server)去更新事务消息的最终状态:

    • 根据sendResult找到Prepared消息 ,sendResult包含事务消息的ID
    • 根据localTransaction更新消息的最终状态

    如果endTransaction方法执行失败,数据没有发送到broker,导致事务消息的 状态更新失败,broker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法来决定是回滚事务还是继续执行,最后调用endTransactionOneway让broker来更新消息的最终状态。

    再回到转账的例子,如果Bob的账户的余额已经减少,且消息已经发送成功,Smith端开始消费这条消息,这个时候就会出现消费失败和消费超时两个问题,解决超时问题的思路就是一直重试,直到消费端消费消息成功,整个过程中有可能会出现消息重复的问题,按照前面的思路解决即可。

    消费事务消息
    这样基本上可以解决消费端超时问题,但是如果消费失败怎么办?阿里提供给我们的解决方法是:人工解决。大家可以考虑一下,按照事务的流程,因为某种原因Smith加款失败,那么需要回滚整个流程。如果消息系统要实现这个回滚流程的话,系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比消费失败的概率大很多。这也是RocketMQ目前暂时没有解决这个问题的原因,在设计实现消息系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,这也是大家在解决疑难问题时需要多多思考的地方。

    我们需要注意的是,在3.2.6版本中移除了事务消息的实现,所以此版本不支持事务消息。也就是说,消息失败不会进行检查。

    7、交易事务处理示例

    下面我们来看一个简单的例子:

    消息生产者:

    public class TransactionProducer {
        public static void main(String[] args) throws MQClientException, InterruptedException {
            TransactionListener transactionListener = new TransactionListenerImpl();
            TransactionMQProducer producer = new TransactionMQProducer("transaction_producer");
            //设置用于事务消息的处理线程池
            ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setName("client-transaction-msg-check-thread");
                    return thread;
                }
            });
            producer.setNamesrvAddr("127.0.0.1:9876");
            producer.setExecutorService(executorService);
            //设置事务监听器,监听器实现接口org.apache.rocketmq.client.producer.TransactionListener
            //监听器中实现需要处理的交易业务逻辑的处理,以及MQ Broker中未确认的事务与业务的确认逻辑
            producer.setTransactionListener(transactionListener);
            producer.start();
    
            //生成不同的Tag,用于模拟不同的处理场景
            String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
            for (int i = 0; i < 10; i++) {
                try {
                	//组装产生消息
                    Message msg =
                        new Message("TopicTransaction", tags[i % tags.length], "KEY" + i,
                            ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                    //以事务发送消息,并在事务消息被成功预写入到RocketMQ中后,执行用户定义的交易逻辑,
                    //交易逻辑执行成功后,再实现实现业务消息的提交逻辑
                    SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                    System.out.printf("%s%n", sendResult);
                    System.out.printf("%s%n", sendResult.getTransactionId());
                    Thread.sleep(10);
                } catch (MQClientException | UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
    
            producer.shutdown();
        }
    }

    业务实现类TransactionListenerImpl:

    public class TransactionListenerImpl implements TransactionListener {
        private AtomicInteger transactionIndex = new AtomicInteger(0);
    
        private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
    
        /**
         * 该方法会在消息成功预写入RocketMQ后被执行
         */
        @Override
        public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
            int value = transactionIndex.getAndIncrement();
            System.out.println("开始处理业务逻辑...");
            int status = value % 3;
            localTrans.put(msg.getTransactionId(), status);
            switch (status) {
    	        case 0:
    	        	//LocalTransactionState.UNKNOW表示未知的事件,需要RocketMQ进一步服务业务进行确认该交易的处理
    	        	//结果,确认消息被调用的方法为下方的checkLocalTransaction。
    	        	//注:RocketMQ与业务确认消息的执行状态的功能已经被移除了,在早期3.0.8的版本中有该功能,因而如果
    	        	//返回的状态为UNKNOW,则该消息不会被提交
    	        	return LocalTransactionState.UNKNOW;
    	        case 1:
    	        	return LocalTransactionState.COMMIT_MESSAGE;
            	case 2:
            		return LocalTransactionState.ROLLBACK_MESSAGE;
            	default:
            		return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
    
        /**
         * 该方法用于RocketMQ与业务确认未提交事务的消息的状态,不过该方法已经的实现在RocketMQ中已经
         * 被删除了,因而其功能也就没有意义了。
         * 不过如果使用阿里云的企业的RocketMQ服务,该功能会起作用。
         */
        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt msg) {
            Integer status = localTrans.get(msg.getTransactionId());
            int mod = msg.getTransactionId().hashCode() % 2;
            if (null != status) {
                switch (mod) {
                    case 0:
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    case 1:
                        return LocalTransactionState.COMMIT_MESSAGE;
                    default:
                        return LocalTransactionState.COMMIT_MESSAGE;
                }
            }
            return LocalTransactionState.COMMIT_MESSAGE;
        }
    }

    消息消费者:

    public class Consumer {
    	public static void main(String[] args) throws MQClientException {
    		DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction_producer");
    		consumer.setNamesrvAddr("127.0.0.1:9876");
    		/**
    		 * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
    		 * 如果非第一次启动,那么按照上次消费的位置继续消费
    		 */
    		consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
    		consumer.subscribe("TopicTransaction", "*");
    		consumer.registerMessageListener(new MessageListenerOrderly() {
    			private Random random = new Random();
    
    			@Override
    			public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
    				// 设置自动提交
    				context.setAutoCommit(true);
    				for (MessageExt msg : msgs) {
    					System.out.println("获取到消息开始消费:"+msg + " , content : " + new String(msg.getBody()));
    				}
    				try {
    					// 模拟业务处理
    					TimeUnit.SECONDS.sleep(random.nextInt(5));
    				} catch (Exception e) {
    					e.printStackTrace();
    					//返回处理失败,该消息后续可以继续被消费
    					return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
    				}
    				//返回处理成功,该消息就不会再次投递过来了
    				return ConsumeOrderlyStatus.SUCCESS;
    			}
    		});
    		consumer.start();
    		System.out.println("consumer start ! ");
    	}
    }

    我们先启动消费端,然后启动生产端:

    在运行之前,我们先来看一下,web控制台的消息:

    Topic transaction_producer中没有未消费的消息,下面开始执行代码逻辑。

    生产端:

    通过日志可以看到其发送了四条消息,交易逻辑被调用了4次,其中只有一条消息反馈的结果为COMMIT_MESSAGE。

    再次查看控制台:

    可以看到其Topic transaction_producer只有一条待消费的消息,这个和发送端只一条消息被COMMIT的结论相符合。

    消费端:

    启动消费端,在控制台只看到消费了一条消息。

    生产者总共生产了四条消息,原因如下:

     

     

    这就是为什么我们生产了四条消息,最后却只消费了一条,再次确认业务实现类TransactionListenerImpl中的方法checkLocalTransaction被没有调用。

    8、如何保证扣钱与加钱的事务的最终一致性

    在上面的转账交易逻辑中,存在两个问题:

    1)如果没有使用RocketMQ的企业版本,那就可能会发生扣钱的事务成功了,但是扣钱的消息由于生产方发生了故障,导致交易消息没有在扣钱的事务提交成功后往RocketMQ中确认该条消息可以被提交,就会导致该条消息不会递交给消费方,导致Bob的钱被扣了,但是Smith的钱却没有增加。

    2)生产方的全部逻辑都处理完成了,扣钱的事务在数据库中被成功的提交,扣钱的消息在RocketMQ被成功的确认,但是消费方在消费消息的时候,自己本身发生了故障,或者处理该条消息发生了逻辑错误,导致Smith的钱没有被正确的加上。

    以上两个问题虽然发生的机率都很低,但是只要存在着发生的机率就会一定在某个时间点发生,只是故障发生时间点的早晚问题。在金融系统中,每日都会跑系统日志执行对账操作,用于核对当日总共的支付与收入是否是平衡的、每个单笔交易结果是否都满足借贷平衡等,因而为了避免以上两个问题的发生,我的处理方式还是引入金融系统对账的业务逻辑来进行处理。

    其业务处理逻辑如下:

    在发送交易事务消息过后,发送一个交易对账消息到对账Topic中,该对账消息为非事务消息,发送成功即表示成功保存到了RocketMQ中,该交易对账消息不会用于消费者消费,后续的交易对账系统会消费该队列中的对账信息,其分别会和交易的生产方和消费方进行交易核对,核对逻辑如下:

    交易对账系统首先和交易的消费方进行核对,如果消费方消费成功,则可以说明整个交易结果满足最终一致性,因为消息是生产者成功处理后,然后再发送的交易确认消息,因而只要产生了事务确认的交易消息,则可以肯定生产方已经正常执行完了扣款的逻辑。

    只有交易消息在消费方处理失败或者消息方没有消费该消息的情况下,才需要再次和生产方进行确认,如果生产方成功执行了扣款操作,则需要回滚这笔扣款交易;如果没有扣除成功,则表示两边都没有消费这边交易,就不用做任何操作了。

    主要内容来源:https://www.jianshu.com/p/cc5c10221aa1

    展开全文
  • 保证最终一致性概述一、事务型消息原理1.1 消息队列简介1.2 消息队列应用实例1.3 事务型消息设计方案1.4 事务型消息总结二、操作流水2.1 库存...2.4 后置流程2.5 交易单逻辑异步化三、异步更新库存四、事务型消息应用...
  • 在上一篇文章中说到过,Java事务的类型有三种:JDBC事务、JTA(Java Transaction API)事务、容器事务。这是从事务的实现角度区分的,本文从另外一个角度来再次区分一下Java中的事务。 站在事务管理的角度,可以把Java...
  • MySQL分布式事务(XA事务

    千次阅读 2017-08-25 10:57:32
    MySQL分布式事务(XA事务) 官网:https://dev.mysql.com/do...
  • 让我们首先定义一个表现系统中典型的交易帐户的实体(为了简单起见,我们将实例变量都定义为public,但在实际应用中并不存在这种限制): public class TradeAccount { public final long id; ...
  • 分布式事务之柔性事务

    千次阅读 2018-08-19 14:35:40
    定义 在电商领域等互联网场景下,传统的事务在数据库性能和处理能力上都暴露出了瓶颈。在分布式领域基于CAP理论以及BASE理论,有人就提出了柔性事务的概念。CAP(一致性、可用性、分区容忍性)理论大家都理解很多...
  • java事务 spring事务 分布式事物

    千次阅读 2018-11-06 23:37:10
    1分布式事务与解决方案 1 数据库事务 2 Spring 事务传播行为(总结): 2 分布式事务: 1 基于 XA 协议的两段式提交(2PC): 2 事务两阶段提交的过程如下:  3 补偿事务(TCC): 1 本地消息表(MQ 异步...
  • 数据库金额交易事务回滚) import java.sql.*; import java.util.Scanner; public class jdbc_test { public static void main(String[] args) throws ClassNotFoundException, SQLException { Connection ...
  • Mycat高级进阶---事务支持

    万次阅读 2017-03-22 19:48:51
    Mycat里的数据库事务Mycat 目前没有出来跨分片的事务强一致性支持,目前单库内部可以保证事务的完整性,如果跨库事务, 在执行的时候任何分片出错,可以保证所有分片回滚,但是一旦应用发起commit指令,无法保证所有...
  • 但是对于分布式事务场景,我们不仅仅需要考虑正常逻辑流程,还需要关注小概率的异常场景,如果我们对异常场景缺乏处理方案,可能就会出现数据的不一致性,那么后期靠人工干预处理,会是一个成本非常大的任务,此外,...
  • 事务管理之XA分布式事务管理

    千次阅读 2017-08-05 14:39:51
    Java Transaction API 允许您操作应用程序中的分布式事务(Distributed Transaction)。JTA 中有一组方法,它将传统的 JDBC 调用封装到了两阶段提交(Two-Phase-Commit)协议中。 在异构环境中,您通常会发现一个...
  • 微服务:分布式事务

    千次阅读 多人点赞 2019-09-02 18:16:19
    课程分享: 课程分享:Docker+...这篇文章将介绍什么是分布式事务,分布式事务解决什么问题,对分布式事务实现的难点,解决思路,不同场景下方案的选择,通过图解的方式进行梳理、总结和比较。 相信耐心看...
  • 1、事务定义 事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性( consistency)、隔离性(isolation)和持久性(durability)------- ACID 原子性:执行单元中的操作...
  • 文章目录前言一、事务的概念及特点1.概念2.事务的 ACID 特性2.1 原子性2.2 一致性2.3 隔离性2.4 持久性2.5 小结二、事务之间的相互影响1.脏读2.不可重复读3. 幻读4.丢失更新三、MySQL 及事务隔离级别1.概述2.查询...
  • 本地事务&分布式事务

    千次阅读 2018-12-23 17:57:33
    分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在...
  • 数据库事务和四种隔离级别

    万次阅读 多人点赞 2018-04-18 09:44:19
    什么是事务事务(Transaction):访问并可能更新数据库中各种数据项的一个程序执行单元(unit),它通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起。当在数据库中更改数据成功...
  • Mycat 分布式事务的实现

    万次阅读 2017-01-11 14:21:31
    面对企业应用的海量数据事务处理,是目前最好的开源解决方案。但是如果想让多台机器中的数据保存一致,比较常规的解决方法是引入“协调者”来统一调度所有节点的执行。本文选自《分布式数据库架构及企业实践——基于...
  • 对于高性能的定义,通常可以理解为系统/服务接口响应时间低(rt)且并发量(qps,tps)高.提高性能的主要策略有:选择合理的分布式事务处理机制,数据库的分库分表,读写分离,异步化,缓存,复杂查询走搜索。 交易...
  • Java之:事务

    千次阅读 2016-06-30 15:21:58
    一、文章来由事务在金融领域用的是非常之多的,所以现在非常重要,比如一个事务是“A给B转账500块”...一般的事务指的都是数据库事务,但是广义事务定义不局限于数据库事务。三、事务的特性事务有4大特性,即 ACID。
  • eos代码阅读笔记05- 事务transaction

    千次阅读 2018-06-29 19:20:08
    数据库的事务定义:事务(Transaction)是并发控制的单位,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。  eos的transaction很像数据库的事务。用事务完成交易,账号/权限...
  • 数据库事务控制

    2019-09-06 10:36:00
    数据库事务(datebase transaction),是指作为独立的逻辑工作单元执行的一系列操作,要么完全的执行,要么完全不执行。事务处理可以保证除非事务性单元内的所有操作完全成功执行,否则不会永久更新面向数据的资源。...
  • 常用的分布式事务解决方案

    万次阅读 多人点赞 2018-03-11 14:44:55
    众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用...
  • 分布式事务的解决方案 分布式事务的解决方案有如下几种: 全局消息 基于可靠消息服务的分布式事务 TCC 最大努力通知 方案1:全局事务(DTP模型) 全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式...
  • 文章目录1、配置Spring数据库事务2、数据库隔离级别与传播行为3、声明式事务4、在Spring+MyBatis组合中使用事务 1、配置Spring数据库事务 2、数据库隔离级别与传播行为 3、声明式事务 4、在Spring+MyBatis组合中使用...
  • 事务处理原理 第2版

    热门讨论 2012-12-30 10:49:38
    运输业、金融业、零售业、电信业、制造业、政府部门和军事等各个领域的大型企业都完全依赖事务处理应用程序来完成电子预定服务、银行业务、证券交易、订单处理、音乐和视频服务、运货跟踪、政府服务、电话交换、库存...
  • db2事务隔离级别

    千次阅读 2018-12-14 16:37:42
    Jdbc事务隔离级别 Jdbc隔离级别 数据库隔离级别 数据访问情况 TRANSACTION_READ_UNCOMMITTED(未提交读)Uncommitted Read ur 脏读,在没有提交数据的时候...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,366
精华内容 15,346
关键字:

交易事务的定义