MySQL 事务


1 事务定义

事务是数据库管理系统 (DBMS) 执行过程中的一个逻辑单位, 由一个有限的数据库操作序列构成, 这些操作要么都做, 要么都不做, 是一个不可分割的工作单位。

事务是数据库最小的工作单元, 是不可以再分的, 可能包含了一个或者一系列的 DML 语句。

2 事务的作用

下面列举几个比较常见的作用:

1. 确保数据完整性
事务可以确保一系列操作作为一个整体被执行,要么全部成功,要么全部失败。这对于需要多步操作的复杂业务来说非常重要,如银行转账、订单处理等。

2. 提供错误恢复机制
如果在执行事务的过程中发生错误(例如,系统崩溃或其他问题),数据库可以利用事务日志回滚到事务开始前的状态,避免数据损坏或丢失。

3. 支持并发控制
在多用户环境中,事务提供了一种机制,使得每个用户可以独立地工作,同时防止他们的操作相互干扰。这是通过事务的隔离级别来实现的,例如,读已提交、可重复读、串行化等。

4. 保持业务逻辑的一致性
通过将相关的一系列数据库操作组合成一个事务,可以确保业务逻辑的一致性。如果不使用事务,那么在一系列操作中的某个点发生故障,可能会导致数据状态不一致。

3 事务的四大特性

事务的四大特性: ACID

Atomicity (原子性)
对数据库的一系列的操作, 要么都是成功, 要么都是失败, 不可能出现部分成功或者部分失败的情况。

以转账的场景为例, 一个账户的余额减少, 对应一个账户的增加, 这两个一定是同时成功或者同时失败的。

在 InnoDB 里面是通过 undo log 来实现的, 它记录了数据修改之前的值 (逻辑日志) , 一旦发生异常, 就可以用 undo log 来实现回滚操作

consistent (一致性)
数据库的完整性约束没有被破坏, 事务执行的前后都是合法的数据状态。

比如说转账的这个场景, A 账户余额减少 1000, B 账户余额只增加了 500, 这个时候因为两个操作都成功了, 按照我们对原子性的定义, 它是满足原子性的, 但是它没有满足一致性, 因为它导致了会计科目的不平衡。

Isolation (隔离性)
多个事务, 对表或者行的并发操作, 是透明的, 互相不干扰的, 一个未完成事务不会影响另外一个未完成事务。

Durable (持久性)
一旦一个事务被提交, 它应该持久保存, 不会因为与其他操作冲突而取消这个事务。

持久性是通过 redo log 和 double write 双写缓冲来实现的。
我们操作数据的时候, 会先写到内存的 buffer pool 里面, 同时记录 redo log, 如果在刷盘之前出现异常, 在重启后就可以读取 redo log 的内容, 写入到磁盘, 保证数据的持久性。
恢复成功的前提是数据页本身没有被破坏, 是完整的, 这个通过双写缓冲 (double write) 保证。

4 数据库什么时候会出现事务

1. 自动开启和自动提交

update 表名 set 字段 =where 条件;

上面的 SQL 会自动开启了一个事务, 并且自动提交了, 所以最终写入了磁盘。

这种行为是通过 InnoDB 里面有一个 autocommit 的参数 (show variables like 'autocommit';) 控制的, 它的默认值是 on/true,
表示在操作数据的时候, 会自动开启一个事务, 和自动提交事务。

2. 手动开启和手动提交

把 autocommit 设置成 false/off, 那么数据库的事务就需要我们手动开启和手动结束。

手动开启事务的方法

  1. begin
  2. start transcation

手动提交事务/手动撤销事务

  1. commit
  2. rollback
  3. 客户端的连接断开的时候, 事务也会结束

5 事务并发会带来的问题

5.1 脏读

如图:

Alt '脏读'

  1. Transcation A 查询 id = 1 的记录的 age = 16
  2. Transcation B 更新 id = 1 的记录的 age = 18, 但是还未提交事务
  3. Transcation A 再次查询 id = 1 的记录的 age = 18

在一个事务里面, 由于其他的事务修改了数据并且没有提交, 而导致了前后两次读取数据不一致的情况, 这种事务并发的问题叫做 脏读

5.2 不可重复读

如图:

Alt '不可重复读'

  1. Transcation A 查询 id = 1 的记录的 age = 16
  2. Transcation B 更新 id = 1 的记录的 age = 18, 并且提交了事务
  3. Transcation A 再次查询 id = 1 的记录的 age = 18

一个事务里面, 读取到了其他事务已提交的数据导致前后两次读取数据不一致的情况, 这种事务并发的问题叫做 不可重复读

4.2 幻读

如图:

Alt '幻读'

  1. Transcation A 查询 age > 15 的记录总条数的 count = 1
  2. Transcation B 插入了一条 age = 18 的记录, 并且提交了事务
  3. Transcation A 再次查询 age > 15 的记录总条数的 count = 2

一个事务里面, 由于其他事务插入或者删除数据导致前后两次读取数据数据不一致, 这种事务并发的问题叫做 幻读

幻读和不可重复读的区别:

  1. 不可重复读强调的是记录的修改
  2. 幻读强调的集合的增减

事务并发带来的三大问题, 无论是脏读, 还是不可重复读, 还是幻读, 它们都是数据库的读一致性的问题, 都是在一个事务里面前后两次读取出现了不一致的情况。
读一致性的问题, 必须要由数据库提供一定的事务隔离机制来解决。

6 SQL92 标准

数据库厂商都按照这个标准, 提供一定的事务隔离级别, 来解决事务并发的问题。

SQL92 里面定义了四个隔离级别

  1. Read Uncommitted (未提交读) :, 一个事务可以读取到其他事务未提交的数据, 会出现脏读, 它没有解决任何的问题
  2. Read Committed (已提交读) : 一个事务只能读取到其他事务已提交的数据, 不能读取到其他事务未提交的数据, 它解决了脏读的问题, 但是会出现不可重复读的问题
  3. Repeatable Read (可重复读): 在同一个事务里面多次读取同样的数据结果是一样的, 它解决了不可重复读的问题, 但是会出现幻读的问题
  4. Serializable (串行化) : 在这个隔离级别里面, 所有的事务都是串行执行的, 也就是对数据的操作需要排队, 已经不存在事务的并发操作了, 所以它解决了所有的问题

这个是 SQL92 的标准, 但是不同的数据库厂商或者存储引擎的实现有一定的差异, 比如 Oracle 里面就只有两种 RC (已提交读) 和 Serializable (串行化)

6.1 MySQL InnoDB 对隔离级别的支持

在 MySQL InnoDB 里面, 不需要使用串行化的隔离级别去解决所有问题。

事务隔离级别 脏读 不可重复读 幻读
未提交读 (Read Uncommitted) 可能 可能 可能
已提交读 (Read Committed) 不可能 可能 可能
可重复读 (Repeatable Read) 不可能 不可能 对 InnoDB 不可能
串行化 (Serializable) 不可能 不可能 不可能

InnoDB 支持的四个隔离级别和 SQL92 定义的基本一致, 隔离级别越高, 事务的并发度就越低。
唯一的区别就在于, InnoDB 在 RR 的级别就解决了幻读的问题。
这个也是 InnoDB 默认使用 RR 作为事务隔离级别的原因, 既保证了数据的一致性, 又支持较高的并发度。

7 事务隔离级别的实现

要解决读一致性的问题, 保证一个事务中前后两次读取数据结果一致, 实现事务隔离, 应该怎么做?

7.1 LBCC

要保证前后两次读取数据一致, 那么读取数据的时候, 锁定要操作的数据, 不允许其他的事务修改就行了。这种方案叫做基于锁的并发控制 Lock Based Concurrency Control (LBCC) 。
如果仅仅是基于锁来实现事务隔离, 一个事务读取的时候不允许其他时候修改, 那就意味着不支持并发的读写操作, 而我们的大多数应用都是读多写少的, 这样会极大地影响操作数据的效率。

7.2 MVCC

要让一个事务前后两次读取的数据保持一致, 那么可以在修改数据的时候给它建立一个备份或者叫快照, 后面再来读取这个快照就行了。
这种方案叫做多版本的并发控制 Multi Version Concurrency Control (MVCC) 。

MVCC 的核心思想是: 可以查到在这个事务开始之前已经存在的数据, 即使它在后面被修改或者删除了。在这个事务之后新增的数据, 是查不到的。

MVCC 的实现

InnoDB 为每行记录都实现了三个隐藏字段

  1. DB_ROW_ID: 6 个字节, 行标识
  2. DB_TRX_ID: 6 个字节, 插入或更新行的最后一个事务的事务 ID, 事务编号是自动递增的 (我们把它理解为创建版本号, 在数据新增或者修改为新数据的时候, 记录当前事务 ID)
  3. DB_ROLL_PTR: 7 个字节, 回滚指针 (我们把它理解为删除版本号, 数据被删除的时候, 记录删除的事务 ID)

如图:
Alt 'MVVC 简单示意图'

如上面的演示, 通过版本号的控制, 无论其他事务是插入 / 修改 / 删除, 第一个事务查询到的数据都没有变化。

在 InnoDB 中, MVCC 是通过 Undo log 实现的, MVCC 和锁是协同使用的。
Oracle, Postgres 等等其他数据库都有 MVCC 的实现。

8 支持事务的存储引擎

  1. InnoDB
  2. NDB

  目录