天机阁

MySQL 事务简介

2022-07-13 · 9 min read
MySQL

前言

对于大部分程序员来说,他们的任务就是把现实世界的业务场景映射到数据库世界中。
银行为了存储人们的账户信息而建立一个 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;

kelvin 和 bob 在银行各自开设了账户,俩人在现实世界的资产就会体现在数据库世界的 account 表中。
假设现在 kelvin 有15元,bob 只有5元。

+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | kelvin |      15 |
|  2 | bob    |       5 |
+----+--------+---------+

随着时间的流逝,kelvin 和 bob 可能陆续向银行账户中存钱、取钱或者向别人转账,他们账户中的余额也因此发生变动。

比如,有一次 bob 着急用钱,急忙打电话向 kelvin 借了10块钱。现实世界中的 kelvin 走向银行 ATM机,输入 bob 的账号以及10元转账金额,然后按下确认键之后就拔卡走人了。

对于数据库世界来说,这相当于执行了下面这两条语句:

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

但是这里有个问题:如果上述两条语句只执行了一条时,服务器忽然断电了,这该咋办?把 kelvin 的钱扣了,但是没给 bob 转过去,这问题就大了。
为了解决这个问题,数据库设计者引入了事务的概念。

事务

我们把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称为事务。

事务的四个特性

原子性(Atomicity)

在现实世界中,转账操作是一个不可分割的操作。也就是说,要么压根就没转,要么转账成功,不能存在中间状态,也就是转了一半的这种情况。我们把这种“要么全做,要么全不做”的规则称为原子性。

隔离性(Isolation)

在现实世界中,两次转账状态应该是互不影响的。比如 kelvin 向 bob 同时转了两次5块钱(假设同时操作)。那么最后 bob 的账户里肯定多10块钱,kelvin 账户里肯定少了10块钱。

但是对应到数据库世界中,事情又变得复杂了一些。为了简化问题,我们粗略假设 kelvin 向 bob 转账5块钱的过程如下:

  1. 读取 kelvin 账户的余额到变量 A 中:简写为 read(A)
  2. 将 kelvin 账户的余额减去转账金额:简写为 A = A - 5
  3. 将 kelvin 账户修改过的余额写到磁盘中,简写为 write(A)
  4. 读取 bob 账户的余额到变量 B 中:简写为 read(B)
  5. 将 bob 账户的余额加上转账金额:简写为 B = B + 5
  6. 将 bob 账户修改过的余额写到磁盘中:简写为:write(B)

我们将 kelvin 向 bob 同时进行的两次转账操作分别称为 T1T2
在现实世界中 T1T2 应该是没有关系的,可以先执行完 T1,再执行 T2;或者先执行完 T2,再执行 T1。如下图所示。
转账操作对应的数据库操作

转账操作对应的数据库操作

但是在真实的数据库中,T1 和 T2 的操作可能交替执行,如下图所示。
真实的数据库中T1和T2的操作

真实的数据库中T1和T2的操作
如果按照上面交替执行的T1和T2来看,最终 kelvin 的账户还剩10块钱,相当于只扣了5块钱,但是 bob 的账户里变成了15块,相当于多了10块钱。这样一来,银行怕不是要破产。

所以对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以原子性的方式执行完成,而且要保证其他的状态转换不会影响到本次状态转换,这个规则称为隔离性。

一致性(Consistency)

我们生活的现实世界中存在形形色色的约束,比如身份证号不能重复、性别只能是男或者女、人民币的最大面值只能是100(现在是2022年)、红绿灯只能有3种颜色等。只有符合这些约束的数据才是有效的。
数据库世界只是现实世界的一个映射,现实世界中存在的约束当然也要在数据库世界中有所体现。如果数据库中的数据全部符合现实世界中的约束,我们就说这些数据就是一致的,或者说符合一致性的。

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

  1. 数据库本身能为我们解决一部分一致性需求
    我们知道,MySQL 数据库可以为表建立主键、唯一索引、外键,还可以声明某个列为 NOT NULL 来拒绝 NULL 值的插入。
  2. 更多的一致性需求需要靠写业务代码的程序员自己保证
    现实世界中复杂的一致性需求比比皆是,而由于性能问题把一致性需求交给数据库来解决也是不现实的,所以需要程序员自己保证一部分数据的一致性。

持久性(Durability)

当现实世界中的一个状态转换后,这个转换的结果将永久保留,这个规则被叫做持久性。
当把现实世界的状态转换映射到数据库世界时,持久性意外着该次转换对应的数据库操作所修改的数据都应该在磁盘中保留下来,无论之后发生什么事故,本次转换造成的影响都不应该丢失。

事务的状态

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

  • 活动的(active):事务对应的数据库操作正在执行过程中,我们就说该事务处于活动的状态。
  • 部分提交的(partially committed):当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处于部分提交的状态。
  • 失败的(failed):当事务处于活动的状态或者部分提交状态时,可能遇到某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为停止了当前事务的执行,我们就说该事务处于失败的状态。
  • 中止的(aborted):如果事务执行了半截而变为失败的状态,比如前言中所说的 kelvin 向 bob 转账的事务,当 kelvin 账户的钱被扣除,但是 bob 账户的钱没有增加时遇到了错误,从而导致当前事务处在了失败的状态,那么就需要把已经修改的 kelvin 的账户余额调整为未转账之前的金额。换句话说,就是要撤销失败事务对当前数据库造成的影响。这个撤销的过程用书面点的话说就是:回滚。当回滚操作执行完毕后,也就是数据库恢复到执行事务之前的状态,我们就说该事务处于中止的状态。
  • 提交的(committed):当一个处于部分提交的状态的事务将修改过的数据都刷新到磁盘中之后,我们就可以说该事务处于提交的状态。

事务的状态转换图,如下图所示:
事务的状态转换图

事务的状态转换图

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

写在最后:在 MySQL 中,并不是所有的存储引擎都支持事务的功能,目前只有 InnoDBNDB 存储引擎支持。

🌈扩展知识:

  • InnoDB 引擎使用 redo log 保证事务的持久性,使用 undo log 来保证事务的原子性
  • InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性(默认支持的隔离级别是 REPEATABLE-READ)。
  • 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

本文参考 《MySQL是怎样运行的》 小孩子4919 著