事务处理详解

事务处理几乎在每个信息系统中都会涉及,它存在的意义是为了保证系统中所有的数据都是符合期望的,且相互关联的数据之间不会产生矛盾,数据状态的一致性(consistency)。

要达成数据的一致性要三方面保证:

  • 原子性(Atomic)
  • 隔离性(Isolation)
  • 持久性(durability)

事务不同场景:

  • 当一个服务只使用一个数据源,通过AID保证一致性是最经典的做法。 内部一致性
  • 当一个服务使用到多个不同的数据源,甚至多个服务同时涉及多个不同的数据源。

1. 本地事务

本地事务是指仅操作单一事务资源的不需要全局事务管理器进行协调的事务。直接依赖于数据源本身提供的事务能力来工作。单个服务使用单个数据源情况下

1.1 实现原子性和持久性

数据必须要成功写入磁盘、磁带等持久化存储器后才拥有持久化,存储在内存的数据遇到crash就会丢失。实现原子性和持久性的最大难题就是“写入磁盘”并不是原子的,不仅有“写入”与“未写入”状态,还客观地存在着“正在写”的中间状态

执行事务可能出现以下情况:

  • 未提交事务,写入后崩溃: 程序还没修改完数据,但数据库已经将其中的某些数据变更写入磁盘,此时发生崩溃。撤销修改
  • 已提交事务,写入前崩溃:程序已经修改完事务,提交事务,数据库还没有将数据全部写入磁盘,发生崩溃。 恢复事务修改。

写入中间状态与崩溃都是不可避免的,为了保证原子性和持久性,就只能在崩溃后采取恢复的补救措施,崩溃恢复

为了能够顺利地完成崩溃恢复,在磁盘中写入数据就不能像程序修改内存中变量值那样,直接改变某表某行某列的某个值,而是必须将修改数据这个操作所需的全部信息,包括修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值,等等。以日志的形式——即仅进行顺序追加的文件写入的形式(这是最高效的写入方式)先记录到磁盘中。只有在日志记录全部都安全落盘,数据库在日志中看到代表事务成功提交的“提交记录”(Commit Record)后,才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”(End Record)表示事务已完成持久化,这种事务实现方法被称为“Commit Logging”(提交日志)。

除了日志手段之外,还有一种影子分页的事务实现机制。大体思路是对数据的变更会先写到硬盘的数据中,但并不是直接就地地修改原先的数据,而是将数据复制一份副本,保留原数据,修改副本数据。事务提交之后,修改数据的引用指针。实现更加简单,但涉及到隔离性和并发性时事务并发能力相对有限

Commit Logging的原理清晰,但是Commit Logging存在一个巨大缺陷,所有对数据的真是修改都必须发生在事务提交以后。在此之前即使磁盘有足够空闲、即使某个事务修改的数据量非常大,占用了大量内存缓冲区都不允许提前修改磁盘上的数据。这对提升性能非常不利。

ARIES提出提前写入(Write-Ahead),允许在事务提交之前,提前写入变更数据。

ARIES(基于语义的恢复和隔离算法)按照事务提交时点为界,划分为:

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

Commiting Log允许NO-FORCE不允许STEAL.Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL,它给出的解决办法是增加了另一种被称为 Undo Log 的日志类型,当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值,等等。`以便在事务回滚或者崩溃恢复时根据 Undo Log 对提前写入的数据变动进行擦除.

此前记录的用于崩溃恢复时重演数据变动的日志就相应被命名为 Redo Log,一般翻译为“重做日志”.`

Write-Ahead Logging在崩溃恢复时会执行以下三个阶段的操作:

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

解释一下Transaction Table和Dirty Page Table两大数据库核心数据结构: 用于支持事务的恢复、崩溃恢复和并发管理: 崩溃恢复时会根据日志进行重建

  • Transaction Table: 数据库内存中的一个表,用于记录当前执行或未完全提交的事务的相关信息。
    • Transaction Id:唯一标识一个事务的ID
    • Transaction State: 事务状态。 Active、Committed、Aborted
    • Last LSN:事务最后一次生成的日志序列号,用于定位该事务的最后一个操作。
    • Other Metadata:例如事务开始时间、锁信息等。
  • Dirty Page Table:数据内存中的一个表,用于追踪当前未刷回磁盘的脏页信息。
    • Page Id:标识脏页的唯一ID。
    • RecLSN: 标识该页被首次修改时的日志序列号,用于确定恢复日志的起点。

重做阶段和回滚阶段都应该涉及为幂等的。

1.2 实现隔离性

  • 写锁:如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。
  • 读锁: 多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务加了读锁,允许直接将其升级为写锁,然后写入数据。
  • 范围锁: 对于某个范围直接加排他锁,在这个范围内的数据不能被写入。

并发控制理论: 决定了隔离成都与并发能力是相互抵触的,隔离程度越高,并发访问时的吞吐量就越低。

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

无法方案: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 为空。

不同隔离级别下MVCC规则:

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

2. 全局事务

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

X/Open XA处理事务的架构,其核心内容是定义了全局的事务管理器(用于协调全局事务)和局部的资源管理器(驱动本地事务)之间的通信接口。XA接口是双向的,能在一个事务管理器和多个事务资源管理器之间形成通信桥梁,通过协调多个数据源的一致行动,实现全局事务的统一提交或者回滚。

JAVA中专门定义了基于XA模式实现了全局事务处理标准JTA,主要两个接口:

  • 事务管理器的接口:javax.transaction.TransactionManager.这套接口是给 Java EE 服务器提供容器事务(由容器自动负责事务管理)使用的,还提供了另外一套javax.transaction.UserTransaction接口,用于通过程序代码手动开启、提交和回滚事务。
  • 满足XA规范的资源定义接口: javax.transaction.xa.XAResource,任何资源(JDBC、JMS 等等)如果想要支持 JTA,只要实现 XAResource 接口中的方法即可。

XA将事务提交拆分为两阶段(2pc)过程:

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

XA协议要保证一致性话需要一些其它前提条件:

  • 必须假设网络在提交阶段的短时间内是可靠的,即提交阶段不会丢失消息。同时也假设网络通信在全过程都不会出现误差,不会传递错误的消息。XA协议在准备阶段出现消息丢失可以进行回滚,提交阶段则不行。
  • 必须假设因为网络分区、机器崩溃或者其它原因而导致失联的节点最终能够恢复,不会永久性地处于失联状态。由于在准备阶段已经写入了完整的重做日志,所以当失联机器一旦恢复,就能够从日志中找出已准备妥当但并未提交的事务数据,并向协调者查询该事务的状态,确定下一步应该进行提交还是回滚操作。

两段式提交的缺点:

  • 单点问题: 协调者一旦宕机,所有参与者都必须一直等待。
  • 性能问题:两段提交过程中,所有参与者相当于被绑定成为一个统一调度的整体,期间要经过两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久化,提交阶段在日志写入 Commit Record),整个过程将持续到参与者集群中最慢的那一个处理操作结束为止,这决定了两段式提交的性能通常都较差。
  • 一致性风险: 当网络稳定性和宕机恢复能力假设不成立时,仍可以出现一致性问题。

改进方案(解决两阶段提交的单点问题和准备阶段性能问题),三阶段提交新增了了canCommit阶段(询问状态),评估事务事务是否有可能顺利完成,在回滚场景下三段式性能要好一点,在正常提交状态会差一点,因为多了一次询问。同时在三段式提交中,如果在PreCommit阶段之后发生了协调者宕机,即参与者没有等到DoCommit的消息的话,默认操作策略是提交事务而不是回滚事务或者持续等待。

三阶段式提交对单点问题和回滚时的性能问题有所改善,但是对于一致性风险并没有任何改进,反则增加了。比如在PreCommit之后,协调者发出回滚指令但由于网络问题,有些参与者没有收到,则进行了提交,这就会造成不同参与者之间数据不一致的问题。

3. 共享事务

共享事务是指多个服务共用同一个数据源。数据源是指提供数据的逻辑设备,不必与物理设备一一对应。

方案:

  • 让各个服务共享数据库连接。在同一个应用进程中间共享数据库连接并不困难。数据库连接的基础是网络连接,由IP地址和端口绑定,所以为了实现多个服务共享数据库连接必须保证使用者都在一个应用程序中,所以为了实现共享事务必须添加一个中间角色.通过中间角色去和数据库交互。(考虑可行性? 完全可以成为一个单体项目。 中间角色压力是否多大?)
  • 使用消息队列服务来代替交易服务器。通过消息的消费者来统一处理,实现由本地事务保障的持久化操作。这被称作“[单个数据库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值