3、架构-事务处理

   

目录

   

概述

场景事例

本地事务

实现原子性和持久性

实现隔离性 


概述

    事务处理几乎在每一个信息系统中都会涉及,它存在的意义是为 了保证系统中所有的数据都是符合期望的,且相互关联的数据之间不 会产生矛盾,即数据状态的一致性(Consistency)。按照数据库的经 典理论,要达成这个目标,需要三方面共同努力来保障。

  1. 原子性(Atomic):在同一项业务处理过程中,事务保证了对 多个数据的修改,要么同时成功,要么同时被撤销。
  2. 隔离性(Isolation):在不同的业务处理过程中,事务保证了各 业务正在读、写的数据相互独立,不会彼此影响。
  3. 持久性(Durability):事务应当保证所有成功被提交的数据修 改都能够正确地被持久化,不丢失数据。

以上四种属性即事务的“ACID”特性,但笔者对这种说法其实不 太认同,因为这四种特性并不正交,A、I、D是手段,C是目的,前者 是因,后者是果,弄到一块去完全是为了拼凑个单词缩写。

事务的概念虽然最初起源于数据库系统,但今天已经有所延伸, 不再局限于数据库本身了。所有需要保证数据一致性的应用场景,包 括但不限于数据库、事务内存、缓存、消息队列、分布式存储,等 等,都有可能用到事务。后文里笔者会使用“数据源”来泛指所有这 些场景中提供与存储数据的逻辑设备,但是上述场景所说的事务和一 致性含义可能并不完全一致,说明如下。

  • 当一个服务只使用一个数据源时,通过A、I、D来获得一致性 是最经典的做法,也是相对容易的。此时,多个并发事务所读写的数 据能够被数据源感知是否存在冲突,并发事务的读写在时间线上的最 终顺序是由数据源来确定的,这种事务间一致性被称为“内部一致 性”。
  • 当一个服务使用到多个不同的数据源,甚至多个不同服务同时 涉及多个不同的数据源时,问题就变得困难了许多。此时,并发执行 甚至是先后执行的多个事务,在时间线上的顺序并不由任何一个数据 源来决定,这种涉及多个数据源的事务间一致性被称为“外部一致 性”。

        外部一致性问题通常很难使用A、I、D来解决,因为这样需要付出 很大甚至不切实际的代价;但是外部一致性又是分布式系统中必然会 遇到且必须要解决的问题,为此我们要转变观念,将一致性从“是或 否”的二元属性转变为可以按不同强度分开讨论的多元属性,在确保 代价可承受的前提下获得强度尽可能高的一致性保障,也正因如此, 事务处理才从一个具体操作上的“编程问题”上升成一个需要全局权 衡的“架构问题”。 人们在探索这些解决方案的过程中,产生了许多新的思路和概 念,有一些概念看上去并不那么直观,在本章,笔者会通过同一个场 景事例讲解如何在不同的事务方案中贯穿、理顺这些概念。

场景事例

        Fenix’s Bookstore是一个在线书店。当一本书被成功售出时,需 要确保以下三件事情被正确地处理:

  1. 用户的账号扣减相应的商品款项;
  2. 商品仓库中扣减库存,将商品标识为待配送状态;
  3. 商家的账号增加相应的商品款项。

         接下来,笔者将逐一介绍在“单个服务使用单个数据源”“单个 服务使用多个数据源”“多个服务使用单个数据源”以及“多个服务 使用多个数据源”下,可以采用哪些手段来保证数据在以上场景中被 正确地读写。

本地事务

        本地事务(Local Transaction)其实应该翻译成“局部事务”才 好与稍后的“全局事务”相对应,不过现在“本地事务”的译法似乎 已经成为主流,这里也就不去纠结名称了。本地事务是指仅操作单一 事务资源的、不需要全局事务管理器进行协调的事务。在没有介绍什 么是“全局事务管理器”前,很难从概念入手去讲解“本地事务”, 所以这里先暂且将概念放下,等后面再来对比理解。

        本地事务是一种最基础的事务解决方案,只适用于单个服务使用 单个数据源的场景。从应用角度看,它是直接依赖于数据源本身提供 的事务能力来工作的,在程序代码层面,最多只能对事务接口做一层 标准化的包装(如JDBC接口),并不能深入参与到事务的运作过程 中,事务的开启、终止、提交、回滚、嵌套、设置隔离级别,乃至与 应用代码贴近的事务传播方式,全部都要依赖底层数据源的支持才能 工作,这一点与后续介绍的XA、TCC、SAGA等主要靠应用程序代码来实 现的事务有着十分明显的区别。举个例子,假设你的代码调用了JDBC 中的Transaction::rollback()方法,方法的成功执行也并不一定代表 事务就已经被成功回滚,如果数据表采用的引擎是MyISAM,那 rollback()方法便是一项没有意义的空操作。因此,我们要想深入讨 论本地事务,便不得不越过应用代码的层次,去了解一些数据库本身 的事务实现原理,弄明白传统数据库管理系统是如何通过ACID来实现 事务的。

实现原子性和持久性

        原子性和持久性在事务里是密切相关的两个属性:原子性保证了 事务的多个操作要么都生效要么都不生效,不会存在中间状态;持久 性保证了一旦事务生效,就不会再因为任何原因而导致其修改的内容 被撤销或丢失。

        众所周知,数据必须要成功写入磁盘、磁带等持久化存储器后才 能拥有持久性,只存储在内存中的数据,一旦遇到应用程序忽然崩 溃,或者数据库、操作系统一侧崩溃,甚至是机器突然断电宕机等情 况就会丢失,后文我们将这些意外情况都统称为“崩溃”(Crash)。 实现原子性和持久性的最大困难是“写入磁盘”这个操作并不是原子 的,不仅有“写入”与“未写入”状态,还客观存在着“正在写”的 中间状态。由于写入中间状态与崩溃都不可能消除,所以如果不做额 外保障措施的话,将内存中的数据写入磁盘,并不能保证原子性与持 久性。下面通过具体事例来说明。

        按照前面预设的场景事例,从Fenix’s Bookstore购买一本书需 要修改三个数据:在用户账户中减去货款、在商家账户中增加货款、 在商品仓库中标记一本书为配送状态。由于写入存在中间状态,所以 可能出现以下情形。

  1. 未提交事务,写入后崩溃:程序还没修改完三个数据,但数据 库已经将其中一个或两个数据的变动写入磁盘,若此时出现崩溃,一 旦重启之后,数据库必须要有办法得知崩溃前发生过一次不完整的购 物操作,将已经修改过的数据从磁盘中恢复成没有改过的样子,以保 证原子性。
  2. 已提交事务,写入前崩溃:程序已经修改完三个数据,但数据 库还未将全部三个数据的变动都写入磁盘,若此时出现崩溃,一旦重 启之后,数据库必须要有办法得知崩溃前发生过一次完整的购物操 作,将还没来得及写入磁盘的那部分数据重新写入,以保证持久性。

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

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

        Commit Logging保障数据持久性、原子性的原理并不难理解:首 先,日志一旦成功写入Commit Record,那整个事务就是成功的,即使 真正修改数据时崩溃了,重启后根据已经写入磁盘的日志信息恢复现 场、继续修改数据即可,这保证了持久性;其次,如果日志没有成功 写入Commit Record就发生崩溃,那整个事务就是失败的,系统重启后 会看到一部分没有Commit Record的日志,将这部分日志标记为回滚状 态即可,整个事务就像完全没有发生过一样,这保证了原子性。

        Commit Logging的原理很清晰,也确实有一些数据库就是直接采 用Commit Logging机制来实现事务的,譬如较具代表性的是阿里的 OceanBase。但是,Commit Logging存在一个巨大的先天缺陷:所有对 数据的真实修改都必须发生在事务提交以后,即日志写入了Commit Record之后。在此之前,即使磁盘I/O有足够空闲,即使某个事务修改 的数据量非常庞大,占用了大量的内存缓冲区,无论何种理由,都决 不允许在事务提交之前就修改磁盘上的数据,这一点是Commit Logging成立的前提,却对提升数据库的性能十分不利。为此,ARIES 提出了“提前写入日志”(Write-Ahead Logging)的日志改进方案, 所谓“提前写入”(Write-Ahead),就是允许在事务提交之前写入变 动数据的意思。

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

  1. FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变动数据必须同时完成写入则称为NO-FORCE。 现实中绝大多数数据库采用的都是NO-FORCE策略,因为只要有了日 志,变动数据随时可以持久化,从优化磁盘I/O性能考虑,没有必要强 制数据写入时立即进行。
  2. 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在崩溃恢复时会经历以下三个阶段。

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

        重做阶段和回滚阶段的操作都应该设计为幂等的。为了追求高I/O 性能,以上三个阶段无可避免地会涉及非常烦琐的概念和细节(如 Redo Log、Undo Log的具体数据结构等)。数据库按照是否允许FORCE和STEAL可以产生四种组合,从优化磁 盘I/O的角度看,NO-FORCE加STEAL的组合的性能无疑是最高的;从算 法实现与日志的角度看,NO-FORCE加STEAL的组合的复杂度无疑也是最 高的。

实现隔离性 

        隔离性保证了每个 事务各自读、写的数据互相独立,不会彼此影响。只从定义上就能嗅 出隔离性肯定与并发密切相关,因为如果没有并发,所有事务全都是 串行的,那就不需要任何隔离,或者说这样的访问具备了天然的隔离 性。但现实情况是不可能没有并发,那么,要如何在并发下实现串行 的数据访问呢?几乎所有程序员都会回答:加锁同步呀!正确,现代 数据库均提供了以下三种锁。

  1. 写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为XLock):如果数据有加写锁,就只有持有写锁的事务才能对数据进行 写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加 读锁。
  2. 读锁(Read Lock,也叫作共享锁,Shared Lock,简写为SLock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁 后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍 然可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务 加了读锁,则允许直接将其升级为写锁,然后写入数据。
  3. 范围锁(Range Lock):对于某个范围直接加排他锁,在这个 范围内的数据不能被写入。如下语句是典型的加范围锁的例子:

SELECT * FROM books WHERE price < 100 FOR UPDATE;

        请注意“范围不能被写入”与“一批数据不能被写入”的差别, 即不要把范围锁理解成一组排他锁的集合。加了范围锁后,不仅不能 修改该范围内已有的数据,也不能在该范围内新增或删除任何数据, 后者是一组排他锁的集合无法做到的。 

明天继续...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/702292.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【挑战30天首通《谷粒商城》】-【第一天】10、环境-docker安装mysql

文章目录 课程介绍一、docker 安装 mysql Stage 1&#xff1a;下载镜像文件 Stage 1-1&#xff1a;打开官网查看镜像 Stage 1-2&#xff1a;拉取镜像 Stage 1-3&#xff1a;查看拉取的镜像 Stage 2&#xff1a;创建实例并启动 A&#xff1a;mysql&#xff08;5.7版&#xff09;…

全栈中VUE的install报错说taobao仓库不好使的解决办法

长话短说&#xff0c;就是报错了&#xff0c;直接上干货。 step1&#xff1a;查看设置&#xff0c;主要是看registry npm config getstep2&#xff1a;设置仓库 npm config set registry https://registry.npmmirror.comstep3&#xff1a;再运行步骤一的指令查看仓库是不是变…

语义分割——高分卫星土地覆盖数据集

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑网络重构和应急资源的灾后配电网信息物理系统协调恢复方法》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

【数据结构】队列的实现(链式)

文章目录 队列1.队列的概念及结构概念结构 2.队列的实现&#xff08;链式结构&#xff09;队列定义初始化队列入队出队获取队头元素获取队尾元素销毁队列判断队列是否为空队列有效个数 完整代码&#xff08;包含测试代码&#xff09;Queue.hQueue.ctest.c 队列 1.队列的概念及…

(毫米波雷达数据处理中的)聚类算法(2) – DBSCAN算法及其实践

说明 读者在阅读本文前&#xff0c;建议先看看本系列的第一篇文章&#xff1a;[1]&#xff08;毫米波雷达数据处理中的&#xff09;聚类算法(1) --- 概述-CSDN博客 DBSCAN算法(Density-Based Spatial Clustering of Applications With Noise)是一种基于密度的聚类算法&#xff…

程序设计:控制台输出二叉树 二叉树的形象显示

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 本文指导你编写一个输出到字符…

lerna实战(一)

前言 将大型代码仓库分割成多个独立版本化的 软件包&#xff08;package&#xff09;对于代码共享来说非常有用。但是&#xff0c;如果某些更改 跨越了多个代码仓库的话将变得很 麻烦 并且难以跟踪&#xff0c;并且&#xff0c; 跨越多个代码仓库的测试将迅速变得非常复杂。 …

Android Q - 音频通路调试

对于当前模块不是很清楚&#xff0c;刚好有个项目这方面有点问题&#xff0c;根据展锐支持文档一步步检查就可以了。首先得先弄清楚硬件具体是怎么连接的&#xff0c;比如文档提到的案例&#xff1a;sprd codec speaker output 连接外部 PA。 耳机接的是什么&#xff0c;speake…

Java线程生命周期:Java线程生命周期全景解读

1. 线程生命周期概述&#xff1a;不仅仅是状态转换 在多线程编程中&#xff0c;理解线程的生命周期对于编写有效、高效的代码至关重要。线程生命周期通常描述了线程从创建到死亡的一系列状态变化过程&#xff0c;但其实不仅仅局限于这些状态的简单转换。线程生命周期的理解应该…

Redis的集群模式——Java全栈知识(20)

1、主从模式 Redis 支持主从模式的集群搭建&#xff0c;这是 Redis 提供的最简单的集群模式搭建方案&#xff0c;目的是解决单点服务器宕机的问题。当单点服务器发生故障的时候保证 Redis 正常运行。 主从模式主要是将集群中的 Redis 节点分为主节点和从节点。然后读和写发生在…