「mysql是怎样运行的」第19章 从猫爷被杀说起---事务简介

「mysql是怎样运行的」第19章 从猫爷被杀说起—事务简介

一、事务的起源

概述

对于大部分程序员来说,他们的任务就是把现实世界的业务场景映射到数据库世界。比如银行为了存储人们的账户信息会建立一个 account 表:

CREATE TABLE account (
id INT NOT NULL AUTO_INCREMENT COMMENT '自增id', name VARCHAR(100) COMMENT '客户名称',
balance INT COMMENT '余额',
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;

狗哥和猫爷是一对好基友,他们都到银行开一个账户,他们在现实世界中拥有的资产就会体现在数据库世界的 account 表中。比如现在狗哥有 11 元,猫爷只有 2 元,那么现实中的这个情况映射到数据库的 account 表就是这样:

image-20230126215047586

在某个特定的时刻,狗哥猫爷这些家伙在银行所拥有的资产是一个特定的值,这些特定的值也可以被描述为账户在这个特定的时刻现实世界的一个状态。随着时间的流逝,狗哥和猫爷可能陆续进行向账户中存钱、取钱或者向别人转账等操作,这样他们账户中的余额就可能发生变动,每一个操作都相当于现实世界中账户的一次状态转换。数据库世界作为现实世界的一个映射,自然也要进行相应的变动。不变不知道,一变吓一跳,现实世界中一些看似很简单的状态转换,映射到数据库世界却不是那么容易的。比方说有一次猫爷在赌场赌博输了钱,急忙打电话给狗哥要借10块钱,不然那些看场子的就会把自己剁了。现实世界中的狗哥走向了ATM机,输入了猫爷的账 号以及10元的转账金额,然后按下确认,狗哥就拔卡走人了。对于数据库世界来说,相当于执行了下边这两条语 句:

UPDATE account SET balance = balance - 10 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;

但是这里头有个问题,上述两条语句只执行了一条时忽然服务器断电了咋办?把狗哥的钱扣了,但是没给猫爷转过去,那猫爷还是逃脱不了被砍死的噩运~ 即使对于单独的一条语句,我们前边唠叨 Buffer Pool 时也说过, 在对某个页面进行读写访问时,都会先把这个页面加载到 Buffer Pool 中,之后如果修改了某个页面,也不会立即把修改同步到磁盘,而只是把这个修改了的页面加到 Buffer Pool 的 flush链表 中,在之后的某个时间点才会刷新到磁盘。如果在将修改过的页刷新到磁盘之前系统崩溃了那岂不是猫爷还是要被砍死?或者在刷新磁盘的过 程中(只刷新部分数据到磁盘上)系统奔溃了猫爷也会被砍死?

怎么才能保证让可怜的猫爷不被砍死呢?其实再仔细想想,我们只是想让某些数据库操作符合现实世界中状态转换的规则而已,设计数据库的大叔们仔细盘算了盘算,现实世界中状态转换的规则有好几条,待我们慢慢道来。


原子性(Atomicity)

现实世界中转账操作是一个不可分割的操作,也就是说要么压根儿就没转,要么转账成功,不能存在中间的状态,也就是转了一半的这种情况。设计数据库的大叔们把这种要么全做,要么全不做的规则称之为原子性。但是在现实世界中的一个不可分割的操作却可能对应着数据库世界若干条不同的操作,数据库中的一条操作也可能被分解成若干个步骤(比如先修改缓存页,之后再刷新到磁盘等),最要命的是在任何一个可能的时间都可能发生意想不到的错误(可能是数据库本身的错误,或者是操作系统错误,甚至是直接断电之类的)而使操作执行不下去,所以猫爷可能会被砍死。为了保证在数据库世界中某些操作的原子性,设计数据库的大叔需要费一些心机来保证如果在执行操作的过程中发生了错误,把已经做了的操作恢复成没执行之前的样子,这也是我们后边章节要仔细唠叨的内容。


隔离性(Isolation)

现实世界中的两次状态转换应该是互不影响的,比如说狗哥向猫爷同时进行的两次金额为5元的转账(假设可以 在两个ATM机上同时操作)。那么最后狗哥的账户里肯定会少10元,猫爷的账户里肯定多了10元。但是到对应的数据库世界中,事情又变的复杂了一些。为了简化问题,我们粗略的假设狗哥向猫爷转账5元的过程是由下边几 个步骤组成的:

  • 步骤一:读取狗哥账户的余额到变量A中,这一步骤简写为 read(A) 。
  • 步骤二:将狗哥账户的余额减去转账金额,这一步骤简写为 A = A - 5 。
  • 步骤三:将狗哥账户修改过的余额写到磁盘里,这一步骤简写为 write(A) 。
  • 步骤四:读取猫爷账户的余额到变量B,这一步骤简写为 read(B) 。
  • 步骤五:将猫爷账户的余额加上转账金额,这一步骤简写为 B = B + 5 。
  • 步骤六:将猫爷账户修改过的余额写到磁盘里,这一步骤简写为 write(B) 。

我们将狗哥向猫爷同时进行的两次转账操作分别称为 T1 和 T2 ,在现实世界中 T1 和 T2 是应该没有关系的,可 以先执行完 T1 ,再执行 T2 ,或者先执行完 T2 ,再执行 T1 ,对应的数据库操作就像这样:

image-20230126231157462

但是很不幸,真实的数据库中 T1 和 T2 的操作可能交替执行,比如这样:

image-20230126231622136

如果按照上图中的执行顺序来进行两次转账的话,最终狗哥的账户里还剩 6元钱,相当于只扣了5元钱,但是猫爷的账户里却成了12 元钱,相当于多了10元钱,这银行岂不是要亏死了?

所以对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以 原子性 的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则被称之为 隔离性 。这时设计数据库的大叔们就需要采取一些措施来让访问相同数据(上例中的A账户和B账户)的不同状态转换(上例中的 T1 和 T2 )对应的数据库操作的执行顺序有一定规律,这也是我们后边章节要仔细唠叨的内容。


一致性(Consistency)

我们生活的这个世界存在着形形色色的约束,比如身份证号不能重复,性别只能是男或者女,高考的分数只能在 0~750之间,人民币面值最大只能是100(现在是2019年),红绿灯只有3种颜色,房价不能为负的,学生要听 老师话,吧啦吧啦有点儿扯远了~ 只有符合这些约束的数据才是有效的,比如有个小孩儿跟你说他高考考了 1000分,你一听就知道他胡扯呢。数据库世界只是现实世界的一个映射,现实世界中存在的约束当然也要在数据 库世界中有所体现。如果数据库中的数据全部符合现实世界中的约束(all defined rules),我们说这些数据就是一致的,或者说符合一致性的。

如何保证数据库中数据的一致性(就是符合所有现实世界的约束)呢?这其实靠两方面的努力:

  • 数据库本身能为我们保证一部分一致性需求(就是数据库自身可以保证一部分现实世界的约束永远有效)。

    我们知道 MySQL 数据库可以为表建立主键、唯一索引、外键、声明某个列为 NOT NULL 来拒绝 NULL 值的插 入。比如说当我们对某个列建立唯一索引时,如果插入某条记录时该列的值重复了,那么 MySQL 就会报错并 且拒绝插入。除了这些我们已经非常熟悉的保证一致性的功能, MySQL 还支持 CHECK 语法来自定义约束,比如这样:

    CREATE TABLE account (
    id INT NOT NULL AUTO_INCREMENT COMMENT '自增id', name VARCHAR(100) COMMENT '客户名称',
    balance INT COMMENT '余额',
    PRIMARY KEY (id),
    CHECK (balance >= 0)
    );
    

    上述例子中的 CHECK 语句本意是想规定 balance 列不能存储小于0的数字,对应的现实世界的意思就是银行账户余额不能小于0。但是很遗憾,MySQL仅仅支持CHECK语法,但实际上并没有一点卵用,也就是说即使我们使用上述带有CHECK 子句的建表语句来创建 account 表,那么在后续插入或更新记录时, MySQL 并不会去检查 CHECK 子句中的约束是否成立。

    小贴士:

    其它的一些数据库,比如SQL Server或者Oracle支持的CHECK语法是有实实在在的作用的,每次进行插入或更新记录之前都会检查一下数据是否符合CHECK子句中指定的约束条件是否成立,如果不成立的话就会拒绝插入或更新。

    虽然 CHECK 子句对一致性检查没什么卵用,但是我们还是可以通过定义触发器的方式来自定义一些约束条件 以保证数据库中数据的一致性。

    小贴士:

    触发器是MySQL基础内容中的知识,本书是一本MySQL进阶的书籍,如果你不了解触发器,那恐怕 要找本基础内容的书籍来看看了。

  • 更多的一致性需求需要靠写业务代码的程序员自己保证。

    为建立现实世界和数据库世界的对应关系,理论上应该把现实世界中的所有约束都反应到数据库世界中,但是很不幸,在更改数据库数据时进行一致性检查是一个耗费性能的工作,比方说我们为 account 表建立了一 个触发器,每当插入或者更新记录时都会校验一下 balance 列的值是不是大于0,这就会影响到插入或更新的速度。仅仅是校验一行记录符不符合一致性需求倒也不是什么大问题,有的一致性需求简直变态,比方说 银行会建立一张代表账单的表,里边儿记录了每个账户的每笔交易,每一笔交易完成后,都需要保证整个系统的余额等于所有账户的收入减去所有账户的支出。如果在数据库层面实现这个一致性需求的话,每次发生交易时,都需要将所有的收入加起来减去所有的支出,再将所有的账户余额加起来,看看两个值相不相等。 这不是搞笑呢么,如果账单表里有几亿条记录,光是这个校验的过程可能就要跑好几个小时,也就是说你在 煎饼摊买个煎饼,使用银行卡付款之后要等好几个小时才能提示付款成功,这样的性能代价是完全承受不起的。

    现实生活中复杂的一致性需求比比皆是,而由于性能问题把一致性需求交给数据库去解决这是不现实的,所以这个锅就甩给了业务端程序员。比方说我们的 account 表,我们也可以不建立触发器,只要编写业务的程 序员在自己的业务代码里判断一下,当某个操作会将 balance 列的值更新为小于0的值时,就不执行该操作 就好了嘛!

我们前边唠叨的 原子性隔离性 都会对 一致性 产生影响,比如我们现实世界中转账操作完成后,有一个一致性需求就是参与转账的账户的总的余额是不变的。如果数据库不遵循 原子性要求,也就是转了一半就不转 了,也就是说给狗哥扣了钱而没给猫爷转过去,那最后就是不符合一致性需求的;类似的,如果数据库不遵循隔离性 要求,就像我们前边唠叨隔离性时举的例子中所说的,最终狗哥账户中扣的钱和猫爷账户中涨的钱可能就 不一样了,也就是说不符合一致性需求了。所以说,数据库某些操作的原子性和隔离性都是保证一致性的一种 手段,在操作执行完成后保证符合所有既定的约束则是一种结果。那满足原子性和 隔离性的操作一定就满足一致性 么?那倒也不一定,比如说狗哥要转账20元给猫爷,虽然在满足 原子性 和 隔离性 ,但转账完成了之后 狗哥的账户的余额就成负的了,这显然是不满足 一致性 的。那不满足 原子性隔离性 的操作就一定不满足一致性 么?这也不一定,只要最后的结果符合所有现实世界中的约束,那么就是符合 一致性 的。


持久性(Durability)

当现实世界的一个状态转换完成后,这个转换的结果将永久的保留,这个规则被设计数据库的大叔们称为 持久性 。比方说狗哥向猫爷转账,当ATM机提示转账成功了,就意味着这次账户的状态转换完成了,狗哥就可以拔卡 走人了。如果当狗哥走掉之后,银行又把这次转账操作给撤销掉,恢复到没转账之前的样子,那猫爷不就惨了, 又得被砍死了,所以这个 持久性是非常重要的。

当把现实世界的状态转换映射到数据库世界时, 持久性 意味着该转换对应的数据库操作所修改的数据都应该在 磁盘上保留下来,不论之后发生了什么事故,本次转换造成的影响都不应该被丢失掉(要不然猫爷还是会被砍 死)。


二、事务的概念

为了方便大家记住我们上边唠叨的现实世界状态转换过程中需要遵守的4个特性,**我们把原子性 ( Atomicity )、 隔离性 ( Isolation )、 一致性 ( Consistency )和 持久性 ( Durability )**这四个词 对应的英文单词首字母提取出来就是 A 、 I 、 C 、 D ,稍微变换一下顺序可以组成一个完整的英文单词:ACID 。想必大家都是学过初高中英语的, ACID 是英文酸的意思,以后我们提到 ACID 这个词儿,大家就应该想到原子性、一致性、隔离性、持久性这几个规则。另外,设计数据库的大叔为了方便起见,把需要保证 原子性 、 隔离性 、 一致性 和 持久性 的一个或多个数据库操作称之为一个事务 (英文名是: transaction )

我们现在知道 事务 是一个抽象的概念,它其实对应着一个或多个数据库操作,设计数据库的大叔根据这些操作所执行的不同阶段把事务大致上划分成了这么几个状态:

  • 活动的(active)

    事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

  • 部分提交的(partially committed)

    当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们 就说该事务处在部分提交的状态

  • 失败的(failed)

    当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者 直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态。

  • 中止的(aborted)

如果事务执行了半截而变为失败的状态,比如我们前边唠叨的狗哥向猫爷转账的事务,当狗哥账户的钱被扣除,但是猫爷账户的钱没有增加时遇到了错误,从而当前事务处在了失败的状态,那么就需要把已经修改的狗哥账户余额调整为未转账之前的金额,换句话说,就是要撤销失败事务对当前数据库造成的影响。书面一点的话,我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事 务之前的状态,我们就说该事务处在了 中止的 状态。

  • 提交的(committed)

    当一个处在 部分提交的 状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了 提交的 状态。

随着事务对应的数据库操作执行到不同阶段,事务的状态也在不断变化,一个基本的状态转换图如下所示:

image-20230128120903946

从图中大家也可以看出了,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。

小贴士: 此贴士处纯属扯犊子,与正文没啥关系,纯属吐槽。大家知道我们的计算机术语基本上全是从英文翻译 成中文的,事务的英文是transaction,英文直译就是交易,买卖的意思,交易就是买的人付钱,卖的 人交货,不能付了钱不交货,交了货不付钱把,所以交易本身就是一种不可分割的操作。不知道是哪位 大神把transaction翻译成了事务(我想估计是他们也想不出什么更好的词儿,只能随便找一个了), 事务这个词儿完全没有交易、买卖的意思,所以大家理解起来也会比较困难,外国人理解transaction 可能更好理解一点吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FrozenPenguin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值