参考:https://icyfenix.cn/

本地事务

我们都知道事务有四大原则:原子性、一致性、隔离性、持久性。具体是如何实现的呢?

实现原子性和持久性

事务的原子性和持久性实现方式

  • 通过日志:常见,主流方案
  • Shadow Paging(影子分页),SQLite就采用该种方式

日志两种实现方式:

  • Commit Logging(提交日志)
    • 在日志没有提交之前不允许持久化数据,原因是如果事务回滚,那么持久化的数据是错误的
    • 存在的问题:会占用非常多的磁盘缓冲区,严重拖垮效率
  • Write-Ahead Logging(提前写入)
    • 允许事务未提交就持久化数据,利用undo log实现持久化数据回滚的能力

    • 崩溃恢复步骤:

      从最后一次checkpoint(checkpoint之前的所有数据都已经安全落盘)开始扫描日志,将扫描到的事务分为两个队列:

      • Redo list:对于已经提交的事务,按照日志进行重做;
      • Undo list:对于未提交的事务,将已经执行的操作回滚;

实现隔离性

通过加锁

事务的隔离级别

根据ARIES 理论中定义:

  • 可串行化:对事务所有读、写的数据全都加上读锁、写锁和范围锁
  • 可重复读:对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁。
    • 出现幻读问题

      ✍🏻MySQL InnoDB 的可重复读级别在只读事务中可以完全避免幻读问题

  • 读已提交:对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放
    • 不可重复读问题
  • 读未提交:对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。
    • 脏读问题

以锁为手段来实现隔离性才是数据库表现出不同隔离级别的根本原因。

全局事务XA

XA( eXtended Architecture)是最早提出的分布式事务处理架构,其核心内容是定义了全局的事务管理器(Transaction Manager)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的通信接口

XA 接口是双向的,能在一个事务管理器和多个资源管理器之间形成通信桥梁,通过协调多个数据源的一致动作,实现全局事务的统一提交或者统一回滚。

JAT(JSR 907 Java Transaction API)定义了基于 XA 模式在 Java 语言中的实现了全局事务处理的标准。JTA 最主要的两个接口是:

  • 事务管理器的接口: javax.transaction.TransactionManager
  • 满足 XA 规范的资源定义接口:javax.transaction.xa.XAResource,任何资源(JDBC、JMS 等等)如果想要支持 JTA,只要实现 XAResource 接口中的方法即可。

全局事务建立在单个服务多个数据源的场景下,如用户买书,而书店的用户、商家、仓库分别处于不同的数据源中:

两段式提交

XA 将事务提交拆分成为两阶段过程(两段式提交,2 Phase Commit,2PC):

  • 准备阶段:又叫作投票阶段,在这一阶段,协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复 Prepared,否则回复 Non-Prepared。
    • 这个阶段离真正的提交只差了一个Commit日志记录
  • 提交阶段:又叫作执行阶段
    • 协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;
      • 持久化一条 Commit 记录
    • 否则,任意一个参与者回复了 Non-Prepared 消息或者超时未回复,协调者将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。
      • 根据回滚日志清理已提交的数据

两段式提交存在的问题

  • 单点问题:协调者宕机

  • 性能问题:多次服务调用

    木桶效应:准备阶段必须等待所有数据源都返回成功后,协调者才能统一发出 Commit 命令,所有涉及的锁和资源都需要等待到最慢的事务完成后才能统一释放

  • 一致性风险:没有宕机恢复能力。

    如果协调者在提交了本地的事务后突然断开,就会导致部分数据(协调者的)已提交,但部分数据(参与者的)既未提交,也没有办法回滚,产生了数据不一致的问题。

FLP 不可能原理:如果不能保证宕机恢复能力,那就不存在任何一种分布式协议可以正确地达成一致性结果。(我的理解)

三段式提交

三段式提交把准备阶段再细分为两个阶段,分别称为 CanCommit、PreCommit,把提交阶段改称为 DoCommit 阶段。这样做的目的是为了减少两段式中准备阶段这一重量操作带来的开销

  • CanCommit:询问阶段,协调者让每个参与者根据自身状态,评估该事务是否有可能顺利完成。
  • PreCommit:准备阶段,准备提交事务
  • DoCommit:提交阶段,协调者发送Commit指令

三段式提交的影响

  • 改善单点问题:如果协调者故障,那么参与者默认会提交事务
  • 减少回滚:先让参与者判断自身是否有能力提交事务,避免故障导致其他参与者做Prepared无用功
  • 一致性问题没有解决,反而加重

分布式事务

这里特指多个服务同时访问多个数据源的事务处理机制

CAP和Base

一个分布式的系统中,涉及共享数据问题时,以下三个特性最多只能同时满足其中两个

  • 一致性
  • 可用性
  • 分区容忍性

  • CA架构:意味着假设节点之间的通信永远是可靠的,必定不可能实现
    • 例子:传统的关系数据库集群,他们不依赖网络实现共享,而是通过日志,如MySQL的bin log
  • CP架构:一般用于对数据质量很高的场合中
    • eg:HBase、前面讨论的全局事务
  • AP架构:主流选择,放弃强一致性,选择最终一致性

最终一致性的起源:BASE理论

BASE 分别是基本可用性、柔性事务和最终一致性的缩写

可靠事件队列

可靠消息队列是通过持续重试来保证可靠性的一种方案,也被称为「最大努力交付」,如TCP的ACK重传机制

可靠事件队列只要第一步业务完成了,后续就没有失败回滚的概念,只许成功,不许失败。

可靠消息队列虽然能保证最终的结果是相对可靠的,但完全没有隔离性可言,如可能导致「超售」问题

TCC 事务

TCC (Try-Confirm-Cancel)是另一种常见的分布式事务机制,具有较强的隔离性

TCC的三个阶段:

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

优势:TCC 在业务执行时只操作预留资源,几乎不会涉及锁和资源的争用,具有很高的性能潜力。

缺点:与业务耦合较高

SAGA 事务

解决TCC第一步无法施行的场景

由于中国网络支付日益盛行,现在用户和商家在书店系统中可以选择不再开设充值账号,如果用户、商家的账号余额由银行管理的话,通常无法完成冻结款项、解冻、扣减这样的操作,因为银行一般不会配合你的操作。所以 TCC 中的第一步 Try 阶段往往无法施行。

SAGA 由两部分操作组成:

  • 大事务拆分若干个小事务,将整个分布式事务 T 分解为 n 个子事务,命名为 T1,T2,…,Ti,…,Tn。每个子事务都应该是或者能被视为是原子行为。如果分布式事务能够正常提交,其对数据的影响(最终一致性)应与连续按顺序成功提交 Ti等价。

  • 为每一个子事务设计对应的补偿动作,命名为 C1,C2,…,Ci,…,Cn。Ti与 Ci必须满足以下条件:

    • Ti与 Ci都具备幂等性。

    • Ti与 Ci满足交换律,即先执行 Ti还是先执行 Ci,其效果都是一样的。

    • Ci必须能成功提交,即不考虑 Ci本身提交失败被回滚的情形,如出现就必须持续重试直至成功,或者要人工介入。

如果 T1到 Tn均成功提交,那事务顺利完成,否则,要采取以下两种恢复策略之一:

  • 正向恢复:如果 Ti事务提交失败,则一直对 Ti进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,譬如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
  • 反向恢复:如果 Ti事务提交失败,则一直执行 Ci对 Ti进行补偿,直至成功为止(最大努力交付)。这里要求 Ci必须(在持续重试后)执行成功。反向恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。

SAGA 必须保证所有子事务都得以提交或者补偿


GTS的AT事务模式:基于数据补偿来代替回滚的思路

解决了XA 2PC带来的木桶效应

基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。这种异步提交的模式,相比起 2PC 极大地提升了系统的吞吐量水平。而代价就是大幅度地牺牲了隔离性,甚至直接影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚并不一定是总能成功的。譬如,当本地事务提交之后、分布式事务完成之前,该数据被补偿之前又被其他操作修改过,即出现了脏写(Dirty Writ e),这时候一旦出现分布式事务需要回滚,就不可能再通过自动的逆向 SQL 来实现补偿,只能由人工介入处理了。🤨