分布式事务

事务基础概念

事务特性

事务就是用户定义的一系列执行SQL语句的操作, 这些操作要么完全地执行,要么完全地都不执行, 它是一个不可分割的工作执行单元

事务的四大特性:

  • 原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚

  • 一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态(比如转账,一个扣减金额,一个增加金额,但是总金额保持不变)

  • 隔离性(Isolation):通常来说,一个事务所做的修改操作在提交事务之前,对于其他事务来说是不可见的

  • 持久性(Durability):一旦事务提交,则其所做的修改会永久保存到数据库

本地事务

如果事务是单阶段事务,并且由数据库直接控制的事务,则属于本地事务

分布式事务

分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。例如创建订单扣减库存事务

分布式事务产生的场景

  1. 微服务架构:微服务之间通过远程调用完成事务操作(即跨JVM进程产生分布式事务)。比如订单微服务下单时需求请求库存微服务扣减库存
  2. 单体系统访问多个数据库实例:当单体系统访问多个数据库实例进行事务操作时就会产生分布式事务(即跨数据库实例产生分布式事务)。比如用户信息和订单信息分别存放在两个数据库,用户管理系统删除用户信息还需要删除对应的用户订单信息,由于两个操作所连接的数据库实例不同,就会产生分布式事务
  3. 多服务访问同一个数据库实例:比如订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,因跨JVM进程,导致两个微服务持有不同的数据库链接进行数据库操作

CAP理论

结合电商系统的业务场景理解CAP

整体执行流程:

  1. 商品服务请求主数据库写入商品信息(添加、修改、删除)
  2. 主数据库向商品服务响应写入成功
  3. 商品服务请求从数据库读取商品信息

CAP简介

  • C(Consistency 一致性):指写操作后的读操作可读取最新的数据状态,当数据分布到多个节点上,从任意节点读取到的数据都是最新状态
    • 商品信息的读写要满足一致性就要实现
      • 商品服务写入主数据库成功,则向从数据库查询新数据也成功。否则,反之
    • 如何实现一致性:
      • 写入主数据库后要将数据同步到从数据库
      • 写入主数据库后,在从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免查从库时查到旧数据
    • 分布式系统一致性的特点
      • 由于存在数据同步的过程,写操作的响应会有延迟
      • 为了保证数据一致性会对资源暂时锁定,待数据同步完成后释放锁定资源
  • A(Availability 可用性):指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误
    • 商品信息读取满足可用性就要实现
      • 从库接收数据查询的请求立即响应查询结果
      • 从库不允许出现响应超时或响应错误
    • 如何实现可用性
      • 写入主库后要将数据同步到从库
      • 由于要保证从库的可用性,不可将从库中的资源锁定
      • 即使数据未完全同步,从库也要返回查询的数据,哪怕时旧数据,没有旧数据可按照约定返回一个默认信息,但不能返回错误或响应超时
    • 分布式系统可用性特点
      • 所有请求都有响应,且不会出现响应超时或响应错误
  • P(Partition tolerance 分区容错性):由于分布式系统各节点通常部署在不同节点,不可避免会出现由于网络问题导致的结点通信失败,此时仍可对外提供服务,这叫分区容错性
    • 商品信息读写满足分区容错性就要实现
      • 主库向从库同步数据失败不影响读写操作
      • 其中一个结点挂掉不影响另一个节点对外提供服务
    • 如何实现分区容错性
      • 尽量使用异步代替同步操作,例如使用异步的方式实现主从库的数据同步
      • 添加从库结点,其中一个从节点挂掉,其他从节点提供服务
    • 分布式系统分区容错性特点
      • 分区容错性是分布式系统具备的基本能力

CAP组合方式

在所有分布式事务场景中不会同时具备CAP三个特性,因具备P的前提下C和A是不能共存的

  • AP放弃一致性,追求分区容错性和可用性。通常实现AP都会保证最终一致性--常用的方案
    • 只要用户可以接收所查询的数据在一定时间内不是最新的即可。比如订单退款,今日退款成功,但明日账户到账
  • CP放弃可用性,追求一致性和分区容错性
    • 比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成
  • CA放弃分区容错性,不考虑由于网络不同或结点挂掉的问题,则可实现一致性和可用性(完全可以使用数据库来控制了)

BASE理论

BASE是 Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性)的缩写,是对CAP中的AP的扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用,但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务称之为柔性事务

  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如交易付款出现问题,但不影响商品的正常浏览
  • 软状态:由于不要求强一致性,所以BASE允许系统存在中间状态,这个状态不影响系统可用性,如订单中的支付中、数据同步中等状态,待数据最终一致后状态改为成功状态
  • 最终一致:是指经过一段时间后,所有节点数据都将达到一致。如订单的支付中状态,最终变为支付成功或者支付失败,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待

分布式事务解决方案

两阶段提交(2PC)

简介

2PC是将整个事务流程分为两个阶段:

  • 准备阶段:事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志此时事务未提交
    • Undo日志是记录修改前的数据,用于数据库回滚
    • Redo日志是记录修改后的数据,用于提交事务后写入数据文件
  • 提交阶段:如果事务管理器收到了参与者的执行失败或超时消息时,直接给每个参与者发送回滚消息,否则发送提交消息。参与者根据事务管理器的执令执行提交或回滚操作,并释放事务处理过程中使用的锁资源
    • 注意:必须是最后阶段释放锁资源

解决方案

XA方案

以新用户注册送积分为例说明XA方案

执行流程如下:

  1. 应用程序(AP)持有用户库和积分库两个数据源
  2. 应用程序(AP)通过TM通知用户库RM新增用户,同时通知积分库RM为该用户新增积分,RM此时并未提交事务,此时用户和积分资源锁定
  3. TM收到执行回复,只要一方失败则分别向其他RM发起事务回滚,回滚完毕,资源释放
  4. TM收到执行回复,全部成功,此时向所有RM发起提交事务,提交完毕,资源锁释放

2PC的传统方案是在数据库层面实现的,如Oracle、MySQL都支持2PC协议,为了统一标准,国际开放标准组织定义了分布式事务处理模型DTP。DTP模型定义如下角色:

  • AP:应用程序,可以理解为使用DTP分布式事务的程序
  • RM:资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行可控制,资源管理器控制着分支事务
  • TM:事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即一个全局事务

以上三个角色间交互方式

  • TM向AP提供应用程序编程接口,AP通过TM提交及回滚事务
  • TM交易中间件通过XA接口来通知RM数据库事务的开始、结束、提交、回滚等

DTP模型定义TM和RM之间的通讯接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案

XA方案缺点:

  1. 需要本地数据库支持XA协议
  2. 资源锁需要等待两个阶段结束才释放,性能差

Seata方案

Seata通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要邮件是性能较好,且不长时间占用连接资源,以高效并且对业务无侵入方式解决微服务场景下的分布式事务问题,他目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。

Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外通常分支事务本身就是一个关系数据库的本地事务。

Seata定义了三个组件来协议分布式事务的处理过程

  • TC:事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交和回滚,负责与RM通信协调各个分支事务的提交和回滚
  • TM:事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令
  • RM:控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分支(本地)事务的提交和回滚

以新用户注册送积分为例说明Seata方案

  1. 用户服务的TM向TC申请开启一个全局事务,全局事务创建成功过并生成一个全局唯一的XID
  2. 用户服务的RM向TC注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入XID对应全局事务的管辖
  3. 用户服务执行分支事务,向用户表插入一条记录
  4. 逻辑执行到远程调用积分服务时(XID在微服务调用链路的上下文中传播)。积分服务的RM向TC注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入XID对应全局事务的管辖
  5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务
  6. 用户服务分支事务执行完毕
  7. TM向TC发起针对XID的全局提交或回滚决议
  8. TC调度XID下管辖的全部分支事务完成提交或回滚请求

Seata实现2PC和传统2PC的差别

  • 架构层次方面,传统2PC方案的RM实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而Seata的RM是以jar包形式作为中间件层部署在应用程序的一侧
  • 两阶段提交方案,传统2PC无论第二阶段的决议是提交还是回滚,事务性资源的锁都要保持到第二阶段完成才释放,而Seata的作坊是在第一阶段就将本地事务提交,这可以省去第二阶段持锁的时间,整体提高效率

业务系统集成 Seata 案例:https://c.biancheng.net/springcloud/seata.html


TCC

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作

  • Try操作:做业务检查及资源预留。此阶段仅是一个初步操作,它和后续的Confirm一起才能真正的构成一个完整的业务逻辑
  • Confirm操作:做业务确认操作。Try阶段所有的分支事务执行成功后开始执行Confirm。通常情况下,采用TCC则认为Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需要引入重试机制或进行人工处理,因此需要实现幂等
  • Cancel操作:实现一个与Try相反的操作,即回滚操作,执行分支事务的业务取消了,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。如果Cancel阶段真的出错了,需要引入重试机制或进行人工处理,因此需要实现幂等

TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试

优缺点:

  • TCC 代码耦合度高,需要根据特定的场景和业务逻辑来设计相应的操作。还需要保证接口的幂等操作

  • TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交

TCC需要注意三种异常处理

  • 空回滚:在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功

    • 出现原因:当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚
    • 解决思路:要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚

  • 幂等:为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题

    • 解决思路:在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态
  • 悬挂:就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行
    • 出现原因:在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理

    • 解决思路:如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try

可靠消息最终一致性

可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致

事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。因此可靠消息最终一致性方案要解决以下几个问题:

  1. 本地事务与消息发送的原子性问题:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题
  2. 事务参与方接收消息的可靠性:事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息
  3. 消息重复消费的问题:由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性

解决方案

本地消息表

此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除

以注册送积分为例来说明:用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分

  1. 用户注册:用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致)
    1. begin transaction;
          //1.新增用户
          //2.存储积分消息日志
      commit transation;

      这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性

  2. 定时任务扫描日志:定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试
  3. 消费消息:积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack(即消息确认),否则消息中间件将重复投递此消息。由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性

RocketMQ事务消息方案

RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性

在RocketMQ4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决 Producer 端的消息发送与本地事务执行的原子性问题

以注册送积分的例子来描述整个流程:Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分

  1. Producer 发送事务消息:Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的

    1. 本例中,Producer 发送 ”增加积分消息“ 到MQ Server

  2. MQ Server回应消息发送成功:MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息
  3. Producer 执行本地事务:Producer 端执行业务代码逻辑,通过本地数据库事务控制
    1. 本例中,Producer 执行添加用户操作
  4. 消息投递:MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack
    1. 若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息
    2. 若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“
  5. 事务回查:如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer 来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息

以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可

RocketMQ提供RocketMQLocalTransactionListener接口:

public interface RocketMQLocalTransactionListener {
   /**
    * 发送prepare消息成功此方法被回调,该方法用于执行本地事务
    * @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
    * @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
    * @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
    */
    RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
    
   /**
    * @param msg 通过获取transactionId来判断这条消息的本地事务执行状态
    * @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
    */
    RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}

发送事务消息:以下是RocketMQ提供用于发送事务消息的API

TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//设置TransactionListener实现
producer.setTransactionListener(transactionListener);

//发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);

最大努力通知

发起通知方通过一定的机制最大努力将业务处理结果通知到接收方,即最大努力通知,最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子

交互流程

  1. 账户系统调用充值系统接口
  2. 充值系统完成支付处理向账户系统发起充值结果通知(这个是重点),若通知失败,则充值系统按策略进行重复通知
  3. 账户系统接收到充值结果通知修改充值状态
  4. 账户系统未接收到通知会主动调用充值系统的接口查询充值结果。如果实在通知不到,就提供查询接口,让用户主动去查询一遍

最大努力通知方案的目标:

  • 有一定的消息重复通知机制。 因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
  • 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求

最大努力通知与可靠消息一致性有什么不同:

  1. 解决方案思想不同
    1. 可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证
    2. 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方
  2. 两者的业务应用场景不同
    1. 可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去
  3. 技术解决方向不同
    1. 可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到
    2. 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)

解决方案

采用MQ的ack机制就可以实现最大努力通知

方案一:适合内部系统

本方案是利用MQ的ack机制由MQ向接收通知方发送通知,流程如下:

  1. 发起通知方将通知发给MQ。使用普通消息机制将通知发给MQ
    1. 注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果
  2. 接收通知方监听 MQ
  3. 接收通知方接收消息,业务处理完成回应ack
  4. 接收通知方若没有回应ack则MQ会重复通知
    1. MQ会按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 (如果MQ采用rocketMq,在broker中可进行配置),直到达到通知要求的时间窗口上限
  5. 接收通知方可通过消息校对接口来校对消息的一致性

方案二

利用MQ的ack机制,与方案1不同的是应用程序向接收通知方发送通知

交互流程如下:

  1. 发起通知方将通知发给MQ。使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ
  2. 通知程序监听 MQ,接收MQ的消息
    1. 方案1中接收通知方直接监听MQ,方案2中由通知程序监听MQ。通知程序若没有回应ack则MQ会重复通知
  3. 通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知
    1. 通知程序调用接收通知方案接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消息
  4. 接收通知方可通过消息校对接口来校对消息的一致性

方案1和方案2的不同点:

  1. 方案1中接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知
  2. 方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知

 

posted @ 2024-03-17 14:24  伊文小哥  阅读(114)  评论(0编辑  收藏  举报