Skip to content

事务

本地事务

本地事务是指仅操作单一事务资源的、不需要全局事务管理器进行协调的事务。

从应用角度看,它是直接依赖于数据源本身提供的事务能力来工作的,在程序代码层面,最多只能对事务接口做一层标准化的包装(如 JDBC 接口),并不能深入参与到事务的运作过程当中,事务的操作都要依赖底层数据源的支持才能工作。

例如MySQL的Innodb引擎支持事务操作。

全局事务

被限定为一种适用于单个服务使用多个数据源场景的事务解决方案。

X/A协议,其核心内容是定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的通信接口。通过协调多个数据源的一致动作,实现全局事务的统一提交或者统一回滚。

X/A是规范,JTA是X/A规范的Java实现。

两段式提交(2PC)

准备阶段

协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交(它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record )则回复 Prepared,否则回复 Non-Prepared。

提交阶段

协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。

如果节点因为网络或其他原因处于临时失联状态,由于在准备阶段已经写入了完整的重做日志,所以当失联机器一旦恢复,就能够从日志中找出已准备妥当但并未提交的事务数据,再而向协调者查询该事务的状态,确定下一步应该进行提交还是回滚操作。

缺点

  • 单点问题:如果协调者宕机一直没有恢复,没有正常发送 Commit 或者 Rollback 的指令,那所有参与者都必须一直等待。
  • 性能问题:所有参与者相当于被绑定成为一个统一调度的整体,根据木桶效应,需要等待集群中最慢的那一个处理操作结束为止,导致性能较差。
  • 一致性风险:在协调者会持久化事务状态,并提交自己的事务后,如果这时候网络忽然被断开,无法再通过网络向所有参与者发出 Commit 指令的话,就会导致部分数据(协调者的)已提交,但部分数据(参与者的)既未提交,也没有办法回滚,产生了数据不一致的问题。

三段式提交(3PC)

三段式提交把原本的两段式提交的准备阶段再细分为两个阶段,分别称为 CanCommit、PreCommit,把提交阶段改称为 DoCommit 阶段。新增的 CanCommit 是一个询问阶段,协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。

在三段式提交中,如果在 PreCommit 阶段之后发生了协调者宕机,即参与者没有能等到 DoCommit 的消息的话,默认的操作策略将是提交事务而不是回滚事务或者持续等待,这就相当于避免了协调者单点问题的风险。

缺点

相比两段式提交的一致性风险更高了,因为默认无法接收到协调者的指令时,会自动提交事务,如果因为网络延时导致Abort指令被接受前就提交了事务,反而产生更严重的后果。

分布式事务

分布式服务环境下的事务处理机制(相比全局事务单个服务的多个服务同时访问多个数据源的事务处理机制)。

CAP定理

  • 一致性Consistency):代表数据在任何时刻、任何分布式节点中所看到的都是符合预期的。
  • 可用性Availability):代表系统不间断地提供服务的能力。
  • 分区容错性Partition Tolerance):代表分布式环境中部分节点因网络原因而彼此失联后,即与其他节点形成“网络分区”时,系统仍能正确地提供服务的能力。

只要用到网络来共享数据,分区现象就会始终存在。放弃分区容错性只有不通过网络来实现(例如节点共享同一块磁盘)。而可用性一般是建设分布式的目的(除了银行、证券这些容忍零出错的系统)。所以一致性通常成为了被牺牲的属性。为此划分出了强一致性(即CAP中的C)最终一致性两种类型。

最终一致性:如果数据在一段时间之内没有被另外的操作所更改,那它最终将会达到与强一致性过程相同的结果。

最大努力一次提交

指的就是将最有可能出错的业务以本地事务的方式完成后,采用不断重试的方式(不限于消息系统)来促使同一个分布式事务中的其他关联业务全部完成。

操作流程

  • 应对用户账号扣款、商家账号收款、库存商品出库三个流程,根据出错概率的大小来安排它们的操作顺序。

  • 自己的数据库建立一张消息表,将扣款状态,收款状态,出库状态写入表中。

  • 定时轮询该表,将状态是进行中的发送到用户账号扣款、商家账号收款、库存商品出库三个服务(同步异步皆可),不断轮询直到三个服务执行成功。

    多次请求,要求下游服务的接口需要幂等,且携带全局唯一的事务ID保证扣款等操作只会被执行一次。

缺点

  • 缺少隔离性:在电商系统中,对于超售有着严格的控制,最大努力一次提交不能保证数据的隔离性。

    有可能两个客户在短时间内都成功购买了同一件商品,而且他们各自购买的数量都不超过目前的库存,但他们购买的数量之和却超过了库存。最大努力一次提交无法满足此要求。

  • 不允许失败:没有失败回滚的概念,一直循环直到成功。

TCC(Try-Confirm-Cancel)

它是一种业务侵入式较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。

操作流程

  • Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需用到的业务资源(保障隔离性)。
  • Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。Confirm 阶段可能会重复执行,因此本阶段所执行的操作需要具备幂等性。
  • Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。Cancel 阶段可能会重复执行,也需要满足幂等性。

创建事务,生成事务 ID,记录在活动日志中,进入 Try 阶段,如果服务可达,那么冻结相关资源进入confirm阶段,否则进入cancel阶段。

若进入confirm阶段,完成业务操作,即上步冻结的资源。若此步出现异常,则根据最大努力一次提交原则重复执行。

若进入cancel阶段,释放冻结的资源(也有可能未冻结),若此步出现异常,则根据最大努力一次提交原则重复执行。

缺点

  • 更高的开发成本和业务侵入性

SAGA

TCCTRY步骤如果不能自己定义(对接别人的平台),导致无法施行,只能考虑采用另外一种柔性事务方案:SAGA 事务

操作流程

  • 大事务T拆分为n个小事务,命名为 T1,T2,…,Ti,…,Tn。每个子事务都被视为是原子行为。如果分布式事务能够正常提交,其对数据的影响(最终一致性)应与连续按顺序成功提交 Ti等价。
  • 为每一个子事务设计对应的补偿动作,命名为 C1,C2,…,Ci,…,Cn。Ti与 Ci都具备幂等性,Ti与 Ci满足交换律(即先执行 Ti还是先执行 Ci,其效果都是一样的),Ci必须能成功提交,即不考虑 Ci本身提交失败被回滚的情形,如出现就必须持续重试直至成功,或者要人工介入。
  • 如果 T1到 Tn均成功提交,那事务顺利完成,否则,要采取以下两种恢复策略之一:
    • 正向恢复(Forward Recovery):如果 Ti事务提交失败,则一直对 Ti进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,譬如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,...,Ti(失败),Ti(重试)...,Ti+1,...,Tn。
    • 反向恢复(Backward Recovery):如果 Ti事务提交失败,则一直执行 Ci对 Ti进行补偿,直至成功为止(最大努力交付)。这里要求 Ci必须(在持续重试后)执行成功。反向恢复的执行模式为:T1,T2,...,Ti(失败),Ci(补偿),...,C2,C1。

SAGA 必须保证所有子事务都得以提交或者补偿,但 SAGA 系统本身也有可能会崩溃,所以它必须设计成与数据库类似的日志机制(被称为 SAGA Log)以保证系统恢复后可以追踪到子事务的执行情况

AT

Seata中提供的一种事务模式。通过全局锁(持有全局锁,本地事务才能提交)控制脏写,通过代理 SELECT FOR UPDATE语句实现读隔离(默认读未提交)。

参照了 XA 两段提交协议实现,在业务数据提交时自动拦截所有 SQL,将 SQL 对数据修改前、修改后的结果分别保存快照,生成行锁,通过本地事务一起提交到操作的数据源中,相当于自动记录了重做和回滚日志。

如果分布式事务成功提交,那后续清理每个数据源中对应的日志数据即可;如果分布式事务需要回滚,就根据日志数据自动产生用于补偿的逆向 SQL

相比2PC准备阶段必须等待所有数据源都返回成功后,协调者才能统一发出 Commit 命令导致的性能问题,AT分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。这种异步提交的模式,相比起 2PC 极大地提升了系统的吞吐量水平。