当前位置:网站首页>凤凰架构3——事务处理

凤凰架构3——事务处理

2022-07-06 11:32:00 土拨鼠饲养员

事务处理

本地事务

ARIES理论(Algorithms for Recovery and Isolation Exploiting Semantics,ARIES): 基于语义的恢复与隔离算法

实现原子性和持久性

由于写入中间状态与崩溃都是无法避免的,为了保证原子性和持久性,就只能在崩溃后采取恢复的补救措施,这种数据恢复操作被称为“崩溃恢复”(Crash Recovery)

为了能够顺利地完成崩溃恢复,必须将修改数据这个操作所需的全部信息,以顺序追加的文件写入的形式(这是最高效的写入方式)先记录到磁盘中。日志记录落盘——》数据库在日志看见Commit Record——》对数据进行修改——》日志中加入一条End Record表示事务已完成持久化——》这种实现方式被称为 Commit Logging

Shadow Paging
先将数据复制一份副本,保留原数据,修改副本数据。当事务成功提交,所有数据的修改都成功持久化之后,最后一步是去修改数据的引用指针,将引用从原数据改为新复制出来修改后的副本,最后的“修改指针”这个操作将被认为是原子操作。

Commit Logging的缺点

  • 所有对数据的真实修改都必须发生在事务提交以后,即日志写入了 Commit Record 之后。

ARIES提出“Write-Ahead Logging” 提前写入

Write-Ahead Logging 先将何时写入变动数据,按照事务提交时点为界,划分为 FORCE 和 STEAL 两类情况。

  • FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变动数据必须同时完成写入则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略,因为只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。
  • STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL,不允许则称为 NO-STEAL。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。

Commit Logging 允许 NO-FORCE,但不允许 STEAL。 因为假如事务提交前就有部分变动数据写入磁盘,那一旦事务要回滚,或者发生了崩溃,这些提前写入的变动数据就都成了错误。

Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL。 它给出的解决办法是增加了另一种被称为 Undo Log 的日志类型,当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值,等等。以便在事务回滚或者崩溃恢复时根据 Undo Log 对提前写入的数据变动进行擦除。Undo Log 现在一般被翻译为“回滚日志”,此前记录的用于崩溃恢复时重演数据变动的日志就相应被命名为 Redo Log,一般翻译为“重做日志”。由于 Undo Log 的加入,Write-Ahead Logging 在崩溃恢复时会执行以下三个阶段的操作。

  • 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合,这个集合至少会包括 Transaction Table 和 Dirty Page Table 两个组成部分。
  • 重做阶段(Redo):该阶段依据分析阶段中产生的待恢复的事务集合来重演历史(Repeat History),具体操作为:找出所有包含 Commit Record 的日志,将这些日志修改的数据写入磁盘,写入完成后在日志中增加一条 End Record,然后移除出待恢复事务集合。
  • 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的恢复事务集合,此时剩下的都是需要回滚的事务,它们被称为 Loser,根据 Undo Log 中的信息,将已经提前写入磁盘的信息重新改写回去,以达到回滚这些 Loser 事务的目的。

在这里插入图片描述

实现隔离性

  • 写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为 X-Lock):如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。

  • 读锁(Read Lock,也叫作共享锁,Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务加了读锁,允许直接将其升级为写锁,然后写入数据。

  • 范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。SELECT * FROM books WHERE price < 100 FOR UPDATE;

  • 可串行化(Serializable):对事务所有读、写的数据全都加上读锁、写锁和范围锁即可做到可串行化(“即可”是简化理解,实际还是很复杂的,要分成 Expanding 和 Shrinking 两阶段去处理读锁、写锁与数据间的关系,称为Two-Phase Lock,2PL)

  • 可重复读(Repeatable Read):对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁,相比串行化会存在幻读问题(事务执行过程中,两个完全相同的范围查询得到了不同的结果集),没有范围锁来禁止在该范围内插入新的数据。

Innodb在只读事务可以完全避免幻读,读写事务中,MySQL 仍然会出现幻读问题

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

  • 读未提交(Read Uncommitted):对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。,会引起脏读问题

多版本并发控制 MVCC

针对“一个事务读+另一个事务写”的隔离问题,MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本

版本——》数据库中每一行记录都存在两个看不见的字段:CREATE_VERSION 和 DELETE_VERSION,这两个字段记录的值都是事务 ID,事务 ID 是一个全局严格递增的数值,然后根据以下规则写入数据。

  • 插入数据时:CREATE_VERSION 记录插入数据的事务 ID,DELETE_VERSION 为空。
  • 删除数据时:DELETE_VERSION 记录删除数据的事务 ID,CREATE_VERSION 为空。
  • 修改数据时:将修改数据视为“删除旧数据,插入新数据”的组合,即先将原有数据复制一份,原有数据的 DELETE_VERSION 记录修改数据的事务 ID,CREATE_VERSION 为空。复制出来的新数据的 CREATE_VERSION 记录修改数据的事务 ID,DELETE_VERSION 为空。

如有另外一个事务要读取这些发生了变化的数据,将根据隔离级别来决定到底应该读取哪个版本的数据。

  • 隔离级别是可重复读:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。
  • 隔离级别是读已提交:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。

另外两个隔离级别都没有必要用到 MVCC,因为读未提交直接修改原始数据即可,其他事务查看数据的时候立刻可以看到,根本无须版本字段。可串行化本来的语义就是要阻塞其他事务的读取操作,而 MVCC 是做读取时无锁优化的,自然就不会放到一起用。

MVCC 是只针对“读+写”场景的优化,如果是两个事务同时修改数据,即“写+写”的情况,那就没有多少优化的空间了,此时加锁几乎是唯一可行的解决方案,稍微有点讨论余地的是加锁的策略是“乐观加锁”(Optimistic Locking)还是“悲观加锁”(Pessimistic Locking)。前面笔者介绍的加锁都属于悲观加锁策略,即认为如果不先做加锁再访问数据,就肯定会出现问题。相对地,乐观加锁策略认为事务之间数据存在竞争是偶然情况,没有竞争才是普遍情况,这样就不应该在一开始就加锁,而是应当在出现竞争时再找补救措施。这种思路被称为“乐观并发控制”(Optimistic Concurrency Control,OCC),没有必要迷信什么乐观锁要比悲观锁更快的说法,这纯粹看竞争的剧烈程度,如果竞争剧烈的话,乐观锁反而更慢。

全局事务

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

两阶段提交(2 Phase Commit,2PC)

  • 准备阶段: 协调者询问事务的所有参与者是否准备好提交,回复Prepared或者 Non-Prepared(准备操作对于数据库来说是在重做日志中记录全部事务提交操作所要做的内容,与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record 而已)
  • 提交阶段:协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作

但是2PC成功需要一些前提条件

  • 必须假设网络在提交阶段的短时间内是可靠的,网络通信在全过程都不会出现误差(不会传递错误的消息),投票阶段失败了可以补救(回滚),而提交阶段失败了无法补救(此阶段耗时应尽可能短)
  • 失联节点最终能够恢复,确定下一步是提交还是回滚操作
    在这里插入图片描述

两阶段提交的缺点

  • 单点问题:无可取代(没有重新选举机制),万一在提交阶段之前主挂了,所有的从都受影响;
  • **性能问题:**所有参与者相当于被绑定为一个整体,要经过两次远程服务调用,三次数据持久化(整体时间取决于最慢响应的从
  • 一致性风险: 网络影响导致协调者成功但是无法向参与者发送命令

“三段式提交”(3 Phase Commit,3PC)

三段式提交把原本的两段式提交的准备阶段再细分为两个阶段,分别称为 CanCommit、PreCommit,把提交阶段改称为 DoCommit 阶段。

  • 将原本准备阶段分为 CanCommit、PreCommit。CanCommit 是一个询问阶段,协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。
  • 在需要回滚的场景,3PC的性能比2PC好,但是在正常提交的场景中,3PC性能稍微要比2PC差一点
  • 如果在 PreCommit 阶段之后发生了协调者宕机,默认的操作策略将是提交事务而不是回滚事务或者持续等待,这就相当于避免了协调者单点问题的风险。

在这里插入图片描述

  • 增加的 CanCommit 阶段,可以避免 2PC 的回滚可能性。但对于不需要回滚的情况,这个阶段反而增加了成本;
  • DoCommit 阶段若出现网络分区,从会直接 Commit。但如果刚好主发出的是 Abord(网络问题命令无法传达从),则数据会出现不一致。

共享事务

共享事务(Share Transaction)是指多个服务共用同一个数据源

  • 一种理论可行的方案是直接让各个服务共享数据库连接,作为一个独立于各个服务的远程数据库连接池,或者作为数据库代理
  • 还有一种变种形式:使用消息队列服务器来代替交易服务器。

“共享事务”的提法和这里所列的两种处理方式在实际应用中并不值得提倡

分布式事务(Distributed Transaction)

多个服务同时访问多个数据源的事务处理

CAP 与 ACID

  • 一致性C onsistency):数据在任何时刻、任何分布式节点中所看到的都是符合预期的。
  • 可用性A vailability): 代表系统不间断地提供服务的能力(可靠性(Reliability)和可维护性(Serviceability))
  • 分区容忍性P artition Tolerance): 网络问题失联后仍然能提供服务。

如果舍弃 C、A、P 时所带来的不同影响

  • 如果放弃分区容忍性(CA without P): 在网络环境下,分布式系统的通信一定是不可靠的,分区现象始终存在
  • 如果放弃可用性(CP without A):可以通过2PC/3PC等手段,同时获得分区容忍性和一致性。放弃可用性的系统一般对于数据质量很高的系统
  • 如果放弃一致性(AP without C):选择放弃一致性的 AP 系统目前是设计分布式系统的主流选择

可靠事件队列

通过持续重试来保证可靠性,也有了专门的名字叫作“最大努力交付”(Best-Effort Delivery)

  • TCP 协议中未收到 ACK 应答自动重新发包的可靠性保障就属于最大努力交付
  • 将最有可能出错的业务以本地事务的方式完成后,采用不断重试的方式来促使同一个分布式事务中的其他关联业务全部完成。

TCC 事务(Try-Confirm-Cancel)

可靠消息队列虽然能保证最终的结果是相对可靠的,但完全没有任何隔离性可言(比如超售)

要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程(侵入式较强)

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

TCC带来了更高的灵活性,但是带来了更高的开发成本

SAGA 事务

TCC性能比较好但是代码侵入比较大
SAGA 将一个分布式环境中的大事务分解为一系列本地事务的设计模式

SAGA 由两部分操作组成

  • 大事务拆分若干个小事务,将整个分布式事务 T 分解为 n 个子事务,命名为 T1,T2,…,Ti,…,Tn。每个子事务都应该是或者能被视为是原子行为。如果分布式事务能够正常提交,其对数据的影响(最终一致性)应与连续按顺序成功提交 Ti等价。
  • 为每一个子事务设计对应的补偿动作,命名为 C1,C2,…,Ci,…,Cn。Ti与 Ci必须满足以下条件:
    • Ti与 Ci都具备幂等性。
    • Ti与 Ci满足交换律(Commutative),即先执行 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 事务模式

基于数据补偿来代替回滚的思路

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

  • 成功提交:清理每个数据源中对应的日志数据即可
  • 回滚:日志数据自动产生用于补偿的“逆向 SQL”

比2PC提高了性能,但是牺牲了隔离性,可能出现脏写

原网站

版权声明
本文为[土拨鼠饲养员]所创,转载请带上原文链接,感谢
https://blog.csdn.net/lxsxkf/article/details/125599395