MySQL 事务机制

1. 事务的特性

  • 原子性(Atomicity):事务作为一个整体被执行,要么全部被执行,要么都不执行;
  • 一致性(Consistency):事务应确保数据库从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束;
  • 隔离性(Isolation):数据库允许多个并发事务对数据进行读写的能力,多个事务并发执行时,一个事务的执行不影响其他事务的执行;
  • 持久性(Durability):事务处理结束后,随数据的修改就是永久的。

2. 数据库并发操作引发的问题

  • 脏读:一个事务读到了另一个未提交事务修改过的数据;
    1. data = 1;
    2. 事务 A 开始;
    3. 事务 B 将data的值修改为 2;
    4. 事务 A 读到data = 2
    5. 事务 B 回滚,data = 1
    6. 事务 A 读到了脏数据。
  • 不可重复读:在同一个事务内多次读取同一个数据,结果不同;
    1. data = 1
    2. 事务 A 开始,读取data = 1
    3. 事务 B 开始,修改data = 2,事务 B 提交;
    4. 事务 A 读取到data = 2
    5. 事务 A 中,前后两次读取到的数据不一致。
  • 幻读:事务在做范围查询的过程中,由于另一个事务对范围内新增或删除了记录,导致范围查询的结果条数不一致。
    1. SQL语句:SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10 条;
    2. 事务 A 开始,执行 SQL 语句,结果为 10;
    3. 事务 B 开始,插入一条id = 4的数据,事务 B 提交;
    4. 事务 A 执行 SQL 语句,结果为 11;
    5. 事务 A 前后做的两次范围查询结果不一致。

3. 事务隔离级别

事务隔离级别是数据库管理系统(DBMS)中用于定义事务处理过程中不同事务之间可见性和相互影响程度的一套标准。 事务隔离级别从低到高(性能降低):

  • 读未提交(read uncommitted):最低的隔离级别,指一个事务可以读到另一个事务未提交的数据,存在脏读,不可重复读,幻读问题;
  • 读已提交(read committed):一个事务只能读到已提交事务的数据,可以避免脏读;
  • 可重复读(repeatable read):一个事务执行过程中看到的数据一直和这个是无启动时看到的数据是一致的,没办法完全避免幻读问题(MySQL Innodb 引擎的默认隔离级别);
  • 串行化(serializable):若发生读写重读,后一个事务必须等钱一个事务执行完成,才能继续执行,脏读、不可重复读和幻读都不可能会发生。

4. Read View(MVCC)

4.1. MVCC

MVCC全称Multiversion Concurrency Control,中文为 _多版本并发控制_,是数据库并发控制的解决方案。

在数据库中,对数据的操作分为两种:,数据库的并发场景就分为了:

  • 读-读并发
  • 读-写并发
  • 写-写并发

_ 读 - 读并发 _ 是不会出现问题的,_ 写 - 写并发 _ 常用加锁的方式实现,_ 读 - 写并发 _ 可以通过MVCC的机制解决。

4.2. Read View

前言: 首先先理清楚 _快照读和当前读_:

  • 常用的普通的SELECT语句在不加锁的情况下都是快照读;
  • 其他情况均为当前读;
  • MySQL 中只有读已提交可重复读这两种事务隔离级别才会使用快照读;
    • 读已提交中,每次读取都会重新生成一个 Read View,总是读取行的最新版本;
    • 可重复读中,Read View 会在事务开始时生成,只有在本事务中进行更改才会更新快照。

Read View 主要用来解决可见性的问题,通俗来讲,它会告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。

Read View 中的重要属性字段:

  • trx_idsm_ids):表示在生成 Read View 时,当前系统中活跃且未提交的读写事务的事务 id 列表
  • low_limit_idmax_trx_id):应该分配给下一个事务的 id 值;
  • up_limit_idmin_trx_id):未提交事务中最小的事务 id;
  • creator_trx_id:创建这个_Read View_ 的事务 id。

trx_ids的范围为[up_limit_id, low_limit_id)以及creator_trx_id(就 TM 很离谱,up_limit_id 是下界,low_limit_id 是上界)。

聚簇索引行记录的隐藏字段:

  • db_row_id:隐式主键,如果没给表创建主键,那么就会以这个字段创建聚簇索引;
  • db_trx_id:对这条记录做了最新一次修改的事务 id;
  • db_roll_ptr:回滚指针,指向这条记录的上一个版本(Undolog 中上一个版本的快照地址),通过这个指针可以找到修改前的记录。

以创建 Read View 之后为时间节点,可以将trx_id分为四部分:

  1. trx_id < up_limit_id,已提交的事务的 id;
  2. trx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id IN trx_ids,活跃事务;
  3. trx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id NOT IN trx_ids,已提交事务;
  4. trx_id >= low_limit_id,还未开始的事务。

一个事务访问记录时,除了自己的更新记录总是可见,还有以下的情况:

  1. trx_id < up_limit_id,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,该版本记录对当前事务可见
  2. trx_id >= low_limit_id,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,该版本记录对当前事务不可见
  3. trx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id IN trx_ids的活跃事务,生成该版本的活跃事务还未提交,该版本记录对当前事务不可见
  4. trx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id NOT IN trx_ids的已提交事务,创建 Read View 时,生成该版本记录的已经被提交,所以该版本的记录对当前事务可见
  5. trx_id == creator_trx_id,这 TM 就是本事务 id,肯定可见。

当访问的记录对本事务不可见时,需要从 Undolog 里面获取数据的历史快照,根据事务快照的事务 id 和 Read View 再进行以上比较,如果找到快照则返回,反之,返回空。

Read View 机制通过版本链(Undolog的形式提供控制并发事务访问同一个记录的行为就是 MVCC

4.3. 读已提交和可重复读

4.3.1. 读已提交

当事务隔离级别为读已提交时,每次查询都会创建新的 Read View,由于有 Read View 机制的存在,无法读到未提交事务的记录,但是可以读到其他提交事务的记录:

  1. data = 1
  2. 事务 A 启动;
  3. 事务 B 启动,修改data = 2
  4. 事务 A 查询 data 的值:
    1. 事务 A 创建 Read View
    2. 事务 B 的trx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id IN trx_ids,故事务 B 中修改的事务对 事务 A 不可见;
    3. Undolog 中的历史版本;
    4. 事务 A 查询到的值为data = 1解决了脏读的问题;
  5. 事务 B 提交;
  6. 事务 A 查询 data 的值:
    1. 事务 A 创建 Read View
    2. 事务 B 已提交,其trx_idtrx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id NOT IN trx_ids,故事务 B 修改data后的值对事务 A 可见;
    3. 事务 A 查询到的值为data = 2,前后两次查询结果不一致,发生了不可重复读的问题。
  7. 事务 A 提交,结束。

4.3.2. 可重复读

当事务隔离级别为可重复读时,只会在启动事务时创建一个 Read View,只有在本事务中进行更改才会更新快照。

  1. data = 1
  2. 事务 A 启动,创建 Read View
  3. 事务 B 启动,修改data = 2
  4. 事务 A 查询 data 的值:
    1. 事务 B 的trx_id >= up_limit_id AND trx_id < low_limit_id AND trx_id IN trx_ids,故事务 B 中修改的事务对 事务 A 不可见;
    2. Undolog 中的历史版本;
    3. 事务 A 查询到的值为data = 1解决了脏读的问题;
  5. 事务 B 提交;
  6. 事务 A 查询 data 的值:
    1. 事务 A 使用之前的 Read View
    2. 事务 A 查询到的值为data = 1解决了不可重复读的问题;
  7. 事务 A 提交,结束。

5. 不可重复读下的幻读解决

幻读是指在一个事务内,前后进行同一个范围查询,得到的结果不同。

5.1. 快照读避免幻读

若一个事务中全为快照读,每次范围查询均会查询同一个 Read View,不会出现幻读(MVCC 解决了快照读下的幻读问题)。

  1. id > 0 AND id < 10 的记录有 10 条;
  2. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10;
  3. 事务 B 执行INSERT id VALUE 8 INTO table;,提交;
  4. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10;
  • 解释:事务开始时创建了 Read View,后续的快照读直接读取 Read View,不会发生幻读。

5.2. 当前读避免幻读

一个事务中全为当前读,会在范围查询的字段范围加临键锁(记录锁 + 间隙锁),其他事务修改该范围字段会被阻塞(极易发生死锁),不会出现幻读。

  1. id > 0 AND id < 10 的记录有 10 条;
  2. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10 FOR UPDATE;,结果为 10;
  3. 事务 B 执行INSERT id VALUE 8 INTO table;,会被阻塞(当前读加了临键锁);
  4. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10 FOR UPDATE;,结果为 10;
  • 解释:当前读会对当前读加锁,阻塞其他事务插入、删除、修改操作,故不会发生幻读。

5.3. 仍会发生幻读的场景

当有两个事务,事务 A 先进行快照读,事务 B 插入数据(可以提交),事务 A 进行当前读,就会发生幻读:

  • 场景一:
    1. id > 0 AND id < 10 的记录有 10 条;
    2. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10;
    3. 事务 B 执行INSERT id VALUE 8 INTO table;,提交;
    4. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10;
    5. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10 FOR UPDATE;,结果为 11,发生了幻读;
    • 解释:2.是快照读,读的是 Read View4.是快照读,读的是和2.相同的 Read View,故结果相同;5.是当前读,读取到的数据就有其他事物提交的数据了。
  • 场景二:
    1. id > 0 AND id < 10 的记录有 10 条;
    2. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10;
    3. 事务 B 执行INSERT id VALUE 8 INTO table;,提交;
    4. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 10;
    5. 事务 A 执行UPDATE table SET id = 7 WHERE id = 8;
    6. 事务 A 执行SELECT COUNT(*) FROM table WHERE id > 0 AND id < 10;,结果为 11,发生了幻读;
    • 解释:不可重复读本事务内更新数据时,会重新创建 Read View
  • 解决:事务开始时立马加锁,可有效地避免幻读问题,但容易发生死锁,需慎重考虑

评论