跳过正文
  1. 面试题库/

05|事务

·7528 字·16 分钟
目录
MySQL面试题库 - 这篇文章属于一个选集。
§ 5: 本文

1. MySQL 事务有什么特性?
#

分析

考察事务的 ACID 特性。

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性(Consistency):事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤:从 A 的账户扣除 200 元,对 B 的账户增加 200 元。一致性要求最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况。
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致。因为多个事务同时使用相同数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。

回答

MySQL 事务有 ACID 四大特性,分别是原子性、一致性、隔离性、持久性。

  • 原子性的意思是事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个环节,原子性是由 undo log 日志保证的。
  • 一致性的意思是事务执行前后,数据库的状态必须保持一致性,一致性是通过持久性、原子性、隔离性这三个共同保证的。
  • 隔离性的意思是许多个事务并发读写数据库,可以防止多个事务并发读写同一个数据的时候,导致数据不一致问题的发生,隔离性是由 MVCC 和锁保证的。
  • 持久性的意思是保证事务完成后对数据的修改就是永久的,不会因为系统故障而丢失,持久性是由 redo log 日志保证的。

2. 事务的隔离性如何保证?
#

分析

先说是由 MVCC 和锁实现的,再说一下为什么用 MVCC 和锁能实现隔离性。

回答

事务的隔离性是由 MVCC 和锁保证的。

可重复读隔离级别下的快照读(普通 select),是通过 MVCC 来保证事务隔离性的;当前读(updateselect ... for update)是通过行级锁来保证事务隔离性的。

3. 事务的持久性如何保证?
#

分析

先说是由 redo log 实现的,再说一下为什么用 redo log 能实现持久性。

回答

事务的持久性是由 redo log 保证的,因为 MySQL 通过 WAL(先写日志再写数据)机制,在修改数据的时候,会将本次对数据页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。

Buffer Pool 的脏页会通过后台线程刷盘,即使在脏页还没刷盘的时候发生了数据库重启,由于修改操作都记录到了 redo log,之前已提交的记录都不会丢失,重启后就可以通过 redo log 恢复脏页数据,从而保证了事务的持久性。

4. 事务的原子性如何保证?
#

分析

先说是由 undo log 实现的,再说一下为什么用 undo log 能实现原子性。

回答

事务的原子性是通过 undo log 实现的,在事务还没提交前,历史数据会记录在 undo log 中,如果事务执行过程中出现了错误,或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据,将数据恢复到事务开始之前的状态,从而保证了事务的原子性。

5. MySQL 事务和 Redis 事务有什么区别?
#

分析

Redis 事务没有保证原子性和持久性。

原子性:Redis 事务没有回滚功能,没办法实现跟 MySQL 事务一样的原子性,也就是没办法保证事务执行期间要不全部失败、要不全部成功。如果 Redis 事务执行过程中,中间有命令是错误的,不会停止执行和回滚,这时候事务的执行会出现半成功的状态。

持久性:如果 Redis 使用了 RDB 模式,那么在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也不能保证持久化。如果 Redis 采用了 AOF 模式,因为 AOF 模式的 noeverysecalways 三种配置选项都存在数据丢失的情况,所以不管 Redis 采用什么持久化模式,事务的持久性属性都得不到保证。

回答

MySQL 事务能够实现 ACID 四大特性,而 Redis 事务没有保证原子性和持久性。

Redis 事务没有回滚功能,没办法实现跟 MySQL 事务一样的原子性,也就是没办法保证事务执行期间要不全部失败、要不全部成功。Redis 事务执行过程中,如果中途有命令执行出错,不会停止和回滚,而是继续执行,那么就可能出现半成功的状态。

Redis 不管是 AOF 模式,还是 RDB 快照,都没办法保证数据不丢失,所以 Redis 事务不具有持久性。

6. MySQL 事务隔离级别有哪些?分别解决哪些问题?
#

分析

MySQL 共有四个隔离级别:

  • 读未提交(read uncommitted):指一个事务还没提交时,它做的变更就能被其他事务看到。
  • 读提交(read committed):指一个事务提交之后,它做的变更才能被其他事务看到。
  • 可重复读(repeatable read):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,是 MySQL InnoDB 引擎的默认隔离级别。
  • 串行化(serializable):会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突,后访问的事务必须等前一个事务执行完成,才能继续执行。

按隔离水平高低排序:

alt text

针对不同的隔离级别,并发事务时可能发生的现象也会不同。

alt text

脏读、不可重复读、幻读的意思:

  • 脏读是指一个事务读取了另一个事务还未提交的数据,如果另一个事务回滚,则读取的数据是无效的,脏读可能导致数据的不一致性。
  • 不可重复读是指一个事务多次读取同一条记录,但是在此期间另一个事务修改了该记录,导致前后读取的数据不一致,不可重复读可能导致数据的不一致性。
  • 幻读是指一个事务多次执行同一个查询,但是在此期间另一个事务插入了符合该查询条件的新数据,导致前后查询的结果不一致,幻读可能导致数据的不完整性。

回答

MySQL 默认隔离级别是可重复读,除此之外,MySQL 还支持读未提交、读提交、串行化这三个隔离级别。

事务并发问题存在脏读、不可重复读、幻读这三种,不同的隔离级别解决的问题也各不相同:

  • 读未提交一个问题都没有解决。
  • 读已提交避免了脏读问题,但是还存在不可重复读和幻读这两个问题。
  • 可重复读避免了脏读和不可重复读的问题,不过对于幻读问题是很大程度上避免了,没有完全避免。
  • 串行化是所有问题都可以避免,但是事务的并发性能是最差的。

7. 串行化隔离级别是通过什么实现的?
#

分析

串行化隔离级别是安全性最高的隔离级别,但是也是性能最差的隔离级别,读、写操作都采用加行级别锁的方式来解决脏读、不可重复读、幻读的问题。现实中基本不会用到串行化隔离级别,因为性能太差了,没有 MVCC 机制,读写操作没办法并发。

回答

串行化隔离级别所有 SQL 都会加行级锁,包括普通的 select 查询,都会加 S 型的 next-key 锁。其他事务就没办法对这些已经加锁的记录进行增删改操作了,从而避免了脏读、不可重复读和幻读现象。它的性能是隔离级别中最差的,没有 MVCC 机制,读写操作没办法并发。

8. 脏读和幻读有什么区别?
#

分析

脏读是一个事务读到了另一个未提交事务修改过的数据。

alt text

在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

alt text

回答

  • 脏读是一个事务读到了另一个未提交事务修改过的数据,如果另外一个事务回滚了,刚才读到的数据就与数据库里的数据不一致了。
  • 幻读是前后两次查询的结果集数量不同,比如如果 select 执行了两次,但第二次返回了第一次没有返回的行数据,则该行是“幻像”行。

9. MySQL 默认的隔离级别是什么?怎么实现的?
#

分析

考察可重复读的实现原理。

回答

MySQL 默认的隔离级别是可重复读。

select 查询是通过 MVCC 实现的。在 MVCC 实现中,每条记录都会保存多个版本,每个版本都有一个版本号。事务在读取数据时,会根据事务开始时的版本号来读取数据,从而保证了事务的隔离性。

可重复读隔离级别是在开启事务后,执行第一条 select 语句的时候,会生成一个 Read View,后续事务查询数据的时候都复用这个 Read View,所以保证了事务期间多次读到的数据都是一致的。

10. 介绍一下 MVCC
#

分析

从 MVCC 是什么、解决了什么问题、MVCC 实现原理这三个方向回答。

注意,不用展开讲解可见性规则的判断,不然这个问题要回答很长时间,面试官可能会不耐烦,如果他追问,再去回答。

回答

MVCC 是多版本并发控制,是通过记录历史版本数据,解决读写并发冲突问题,避免了读数据时加锁,提高了事务的并发性能。

MySQL 将历史数据存储在 undo log 中,结构逻辑上类似一个链表。MySQL 数据行上有两个隐藏列,一个是事务 ID,一个是指向 undo log 的指针。

事务开启后,执行第一条 select 语句的时候,会创建 Read View。Read View 记录了当前未提交的事务,通过与历史数据的事务 ID 比较,就可以根据可见性规则进行判断,判断这条记录是否可见。如果可见,就直接将这个数据返回给客户端;如果不可见,就继续往 undo log 版本链查找第一个可见的数据。

11. MVCC 如何判断记录对某一个事务是否可见?
#

分析

Read View 有四个重要字段:

alt text

  • m_ids:指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 ID 列表。注意是一个列表,“活跃事务”指的是启动了但还没提交的事务。
  • min_trx_id:指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 ID 最小的事务,也就是 m_ids 的最小值。
  • max_trx_id:这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 ID 值,也就是全局事务中最大的事务 ID 值加 1。
  • creator_trx_id:指的是创建该 Read View 的事务的事务 ID。

聚簇索引记录中都包含下面两个隐藏列:

alt text

  • trx_id:当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 ID 记录在 trx_id 隐藏列里。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo log 中,然后这个隐藏列是一个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

在创建 Read View 后,可以将记录中的 trx_id 划分为三种情况:

alt text

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
  • 如果记录的 trx_id 值在 Read View 的 min_trx_idmax_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
    • 如果记录的 trx_idm_ids 列表中,表示生成该版本记录的活跃事务依然活跃着,还没提交事务,所以该版本的记录对当前事务不可见。
    • 如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

回答

每一条记录都有两个隐藏列,一个是事务 ID,一个是指向历史数据 undo log 的指针。Read View 有四个字段,分别是创建 Read View 的事务 ID、活跃事务 ID 列表、活跃事务 ID 列表中最小的 ID、下一个事务的 ID。主要有这几种判断规则:

  • 如果记录的事务 ID 小于活跃事务 ID 列表中最小的 ID,就说明该记录是在创建 Read View 前就生成好了,所以该记录是当前事务可见的。
  • 如果记录的事务 ID 大于等于下一个事务的 ID,就说明该记录是在创建 Read View 后才生成的,所以该记录是当前事务不可见的。
  • 如果记录隐藏列的事务 ID 在最小的 ID 和下一个事务的 ID 之间,这时候就需要看记录的事务 ID 是否在活跃事务 ID 列表中:
    • 如果记录的事务 ID 在活跃事务 ID 列表中,说明修改该记录的事务还没提交,所以该记录是不可见的。
    • 如果记录的事务 ID 不在活跃事务 ID 列表中,说明修改该记录的事务已经提交了,那么该记录就是可见的。

12. 读已提交和可重复读隔离级别实现 MVCC 的区别?
#

分析

生成 Read View 的时机不同。

回答

读已提交和可重复读隔离级别都是由 MVCC 实现的,它们的区别在于创建 Read View 的时机不同。

  • 读已提交隔离级别在事务开启后,每次执行 select 都会生成一个新的 Read View,所以每次 select 都能看到其他事务最近提交的数据。
  • 可重复读隔离级别在事务开启后,执行第一条 select 时生成一个 Read View,然后整个事务期间都复用这个 Read View,所以一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的。

13. 为什么互联网公司用读已提交隔离级别?
#

分析

读已提交并发性能更高,因为读已提交没有间隙锁,只有记录锁,而可重复读会有记录锁和间隙锁,所以读已提交隔离级别发生死锁的概率比较小。

回答

读已提交的并发性能更好,因为读已提交没有间隙锁,只有记录锁,发生死锁的概率比较低。然后互联网业务对于幻读和不可重复读的问题都能接受,所以为了降低死锁的概率,提高事务的并发性能,都会选择使用读已提交隔离级别。

14. 可重复读隔离级别是如何解决不可重复读的?
#

分析

分两种查询来回答:

  • 快照读,靠 MVCC 解决不可重复读。
  • 当前读,靠行级锁中的记录锁解决不可重复读。

回答

MySQL 提供了两种查询方式,一种是快照读,就是普通 select 语句,另外一种是当前读,比如 select for update 语句。不同的查询方式,解决不可重复读问题的方式是不一样的。

针对快照读的话,是通过 MVCC 机制来解决的。在可重复读隔离级别下,第一次 select 查询的时候,会生成 Read View,在第二次执行 select 查询的时候,会复用这个 Read View,这样前后两次查询的记录都是一样的,不会读到其他事务更新的操作,这样就不会发生不可重复读的问题。

针对当前读的话,是靠行级锁中的记录锁来实现的。在可重复读隔离级别下,第一次 select for update 语句查询的时候,会对记录加 next-key 锁,这个锁包含记录锁。这时候如果其他事务更新了加了锁的记录,都会被阻塞住,这样就不会发生不可重复读的问题了。

15. 可重复读隔离级别是怎么解决幻读的?
#

分析

分两种查询来回答:

  • 快照读,靠 MVCC 解决幻读。
  • 当前读,靠行级锁中的间隙锁解决幻读。

回答

MySQL 提供了两种查询方式,一种是快照读,就是普通 select 语句,另外一种是当前读,比如 select for update 语句。不同的查询方式,解决幻读问题的方式是不一样的。

针对快照读的话,是通过 MVCC 机制来解决的。在可重复读隔离级别下,第一次 select 查询的时候,会生成 Read View,在第二次执行 select 查询的时候,会复用这个 Read View,这样前后两次查询的结果集都是一样的,不会读到其他事务新插入的记录,这样就不会发生幻读的问题了。

针对当前读的话,是靠行级锁中的间隙锁来实现的。在可重复读隔离级别下,第一次 select for update 语句查询的时候,会对记录加 next-key 锁,这个锁包含间隙锁。这时候如果其他事务往这个间隙插入新记录,都会被阻塞住,这样就不会发生幻读的问题了。

16. 可重复读隔离级别解决了什么问题?有没有完全解决幻读?
#

分析

要强调可重复读隔离级别很大程度上解决了幻读,并没有完全解决幻读。

回答

可重复读隔离级别解决了脏读、不可重复读问题,幻读也很大程度上避免了,但是我觉得并没有完全解决幻读,在一些特殊的场景,还是会发生幻读的问题。

17. 可重复读隔离级别为什么不能完全避免幻读?什么情况下出现幻读?
#

分析

可重复读隔离级别场景下,可能发生幻读。

发生幻读的第一个场景:

alt text

  • 数据库表不存在 id = 5 的记录,事务 A 执行第一次查询的时候,读不到该记录,接着事务 B 插入了 id = 5 的新记录并提交。
  • 事务 A 的更新语句更新了事务 B 刚插入的 id = 5 的这条记录,由于更新操作是当前读,所以事务 A 的更新操作能读到 id = 5 的记录并进行更新。更新的时候,会把 id = 5 这条记录隐藏列的事务 ID 变为事务 A 的事务 ID,代表是事务 A 修改的。
  • 然后事务 A 第二次查询的时候,发现 id = 5 的事务 ID 跟本事务的 ID 是一样的,就会认为是可见的,那么就能读到 id = 5 的数据了,此时就发生了幻读现象。

发生幻读的第二个场景:

  • T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100,得到 3 条记录。
  • T2 时刻:事务 B 往表中插入一个 id = 200 的记录并提交。
  • T3 时刻:事务 A 再执行「当前读语句」:select * from t_test where id > 100 for update,就会得到 4 条记录,此时也发生了幻读现象。

回答

在可重复读隔离级别场景下,当先快照读再当前读的场景下可能会出现幻读的问题。

比如第一个场景,事务 A 通过快照读的方式查询 id = 5 的记录,此时数据库没有这条记录,然后事务 B 向这张表中新插入了一条 id = 5 的记录并提交了事务。接着,事务 A 对 id = 5 这条记录进行更新操作,在这个时刻,这条新记录隐藏列中的事务 ID 就变成了事务 A 的事务 ID,这时候事务 A 再使用 select 语句去查询这条记录时就可以看到这条记录了。这里事务 A 前后两次查询的结果集合数不一样了,于是就发生了幻读。

另外一个场景是,事务 A 通过快照读的方式查询 id > 100 的记录,假设这时候有 1 条记录,然后事务 B 插入了 id = 200 的记录并提交了事务。接着事务 A 通过当前读的方式查询 id > 100 的记录,这时候就会得到 2 条记录,事务 A 前后两次查询的结果集合数不一样了,也发生了幻读。

这两种发生幻读的场景也是可以避免的,尽量在开启事务之后马上执行 select ... for update 语句,因为它会对记录加临键锁(next-key 锁),这样就可以避免其他事务插入新记录,从而避免幻读问题。

18. 可重复读隔离级别,MVCC 完全解决了不可重复读问题吗?
#

分析

不可重复读,代表前后两次查询的记录的值不一样了。

比如表里现在有 id = 1, value = 1 的记录:

  • 事务 A 先执行 select,查询到 id = 1value 是 1。
  • 事务 B 更新 id = 1value 为 2,然后提交事务。
  • 事务 A 执行 select for update,当前读,然后就读到 id = 1, value = 2 的记录了,意味着发生了不可重复读。

回答

如果前后两次查询都是快照读,也就是普通的 select,那就不会产生不可重复读的问题。但是如果第一次查询是快照读,第二次查询是当前读,那么就可能会发生不可重复读的问题。

19. 一个事务里有特别多 SQL 的弊端?
#

分析

一个事务如果有特别多 SQL,那么这个事务就会被称为长事务(大事务)。

长事务有什么影响?

  • 锁定数据过多,容易造成大量的死锁和锁超时: 锁是事务提交的时候才释放的,那么长事务会导致锁持久的时间过长,很容易导致大量的死锁和锁超时的问题。
  • 回滚记录占用大量存储空间,事务回滚时间长: 每条记录在更新的时候都会同时记录一条回滚操作,会产生 undo 日志,而 undo 日志是事务提交并且没有事务依赖的时候才会被清理,那么长事务就会导致 undo 日志堆积很多,占用存储空间,也会导致回滚的时间过长。
  • 执行时间长,容易造成主从延迟: 因为主库上必须等事务执行完成才会写入 binlog,再传给备库。所以如果一个主库上的语句执行 10 分钟,那么这个事务很可能就会导致从库延迟 10 分钟。
  • 并发情况下数据库连接池容易被撑爆: 在长事务中,连接可能会被持续打开,这会占用数据库连接池的资源,可能导致连接池被占满。

怎么查找数据库中的长事务?

可以在 information_schema 库的 innodb_trx 表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。

SELECT *
FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

回答

  • 锁是事务提交的时候才释放的,那么长事务会导致锁持续的时间过长,容易导致大量的死锁和锁超时的问题。
  • 执行事务中每条增删改 SQL 会产生 undo 日志,那么长事务就会导致 undo 日志堆积很多,占用存储空间,也会导致回滚的时间过长。
  • 长事务执行时间过长,容易造成主从延迟。如果一个主库上的语句执行 10 分钟,那么这个事务很可能就会导致从库延迟 10 分钟。
  • 在长事务中,连接可能会被持续打开,这会占用数据库连接池的资源,可能导致连接池被占满。
MySQL面试题库 - 这篇文章属于一个选集。
§ 5: 本文