【Mysql】事务相关的问题

58 阅读8分钟

事务并发

面试老被拷打事务,下决心搞懂它。

事务并发导致的问题详解

首先,我们需要知道事务并发带来什么问题。

先看有哪些问题,再看解释:

  1. 脏读: 事务A读取数据,事务B修改数据未提交,事务A去读取,然后事务B回滚, 事务A读到了不存在的数据,这个叫脏读。
  2. 不可重复读: 事务A读取数据,事务B修改数据并提交, 事务A再读取,前后读取获得的数据不一致
  3. 幻读: 事务A读取数据,事务B插入数据并提交,事务A再读取,前后数据不一致

为了演示,我创建了一个表book 如下:

image.png

下面依次讲解出现的几种问题:

脏读

下面是演示图,画的不好,见谅。

image.png

脏读在mysql数据库中的演示:

  1. 开启事务A(注意设置隔离级别为ru,默认为rr):

image.png

运行第一条select语句,查出结果。

  1. 开启事务B:

image.png

修改值

  1. 事务A再去读取:

image.png 读取到修改后的值

  1. 事务B回滚:

image.png

这个时候事务A读到的数据根本不存在。本来是小红书,但是读到了小黄书。

不可重复读

图示: image.png

在数据库中的演示:

  1. 事务A第一次查询:

image.png

  1. 事务B去进行修改并提交:

image.png

  1. 事务A再去读取发现数据与第一次不一样:

image.png

幻读

图示:

image.png

在数据库中的演示:

  1. 事务A进行第一次查询:

image.png

  1. 事务B进行添加并提交事务:

image.png

  1. 事务A进行第二次读取:

image.png

我们发现事务A第一次读与第二次读获得的数据并不一致。


上面我演示了并发导致的问题,既然有问题,mysql是如何解决的呢?

事务并发问题及其解决(rr和rc隔离级别基于MVCC来解决)

这是一张基本的隔离级别与其对应的能解决的问题。

image.png

当然上面所说的都是宏观层次的设置隔离级别,现在我们去理解理解它的原理MVCC。


官网上有说过,innodb是一个多版本的数据库引擎,它是如何实现的呢?

mysql事务处理事务并发问题

image.png

官网有对这里的说明:

innodb是一个多版本存储引擎。它保留有关已更改行的旧版本的信息以支持事务功能,例如并发和回滚。

它会保存事务执行之前的状态,当事务回滚的时候它会执行undo日志,来恢复之前的数据。

它也会使用这个undo日志信息来,为了持久化的read,会构建一个更早版本的行,官网里这种操作的名字叫做:  Consistent Nonlocking Reads(非阻塞读)。待会我们来具体讲这个。

当前读与快照读

什么是当前读和快照读? 在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读?

当前读:

像select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁.

快照读:

像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

数据库演示一下(RR级别的)快照读:

  1. 事务A第一次读取book表

image.png

  1. 事务B去修改数据 image.png

  2. 事务A第二次读取: image.png

明明已经修改为小黄书,但是读取到的还是小红书。

行添加隐藏字段

innodb对存储在数据库中的行都添加了一些隐藏字段:

image.png

DB_TRX_ID:保存最后一个修改row的事务的id,删除在内部被视为 更新行中特殊位设置为将其标记为删除(因为不是真实删除,所以删除回滚只用清除标记)。

DB_ROLL_PTR: 是一个指针,每次对记录进行修改,都会对应undo log,这个指针就会指向之前的undo log通过它可以找到之前的记录。

DB_ROW_ID: 行id, 当新行插入的时候自动生成row_id。

上面所解释的前两个隐藏id是可重复读的关键。

READVIEW

还有一个关键点read view:

image.png

具体的处理,取决于他们的隔离级别,能使用read view的隔离级别有 可重复读, 读已提交, 读未提交

ReadView中主要包含4个比较重要的内容,分别如下:

  1. creator_trx_id ,创建这个 Read View 的事务 ID。 说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为 事务分配事务id,否则在一个只读事务中的事务id值都默认为0。

  2. trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。

  3. up_limit_id ,活跃的事务中最小的事务 ID。

  4. low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系 统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。

注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。

MVCC 在 RC 隔离级别下的快照读,解决脏读

image.png

在RC隔离级别下,每次select快照读都会创建一个READVIEW对象。

image.png

  1. 如果当前访问事务的id等于creator_trx_id, 表明该事务对其进行了修改,因此该事务可以访问到修改后的数据, 这个过程就是在一个事务中对数据修改又查询,自己的事自己办,所以是可以访问到最新值的。

  2. trx_id < min_trx_id, 当前读到的版本trx_id小于存活事务中最小的,表明该版本已经提交,是可以访问的。

  3. trx_id > max_trx_id, 当前读到的版本trx_id大于存活事务中最大的,表明该版本是在该ReadView创建之后再提交的,不可访问。

  4. max_trx_id >= trx_id >= min_trx_id, 去事务存活列表去寻找,如果不在,表明事务已经提交,(因为事务是依次递增的,在范围里那么它一定是刚提交或者未提交)可以访问,如果在里面,表明该事务还没提交,不可被访问。

    上面出现不可访问的情况就去查上一个版本。

上面这几个步骤就实现了读已提交,解决了脏读的问题

MVCC在RR隔离级别下的快照读,解决不可重复读

只有在第一次快照读时才会创建READVIEW对象,即使一个事务里有十个select语句,也只会创建一个READVIEW对象。

因为每次读取都是使用的同一个READVIEW,所以读取永远和第一次读一样,解决了不可重复读问题。

当然,这个级别肯定也是解决了脏读的问题。

RR隔离级别的MVCC来解决部分幻读问题

事务A连续多次快照读,都会复用同一个READVIEW对象,不管事务B是新增还是修改,读取的还是第一次读取的数据,因此解决了幻读。

但是有一个特例: 当事务A第一次快照读之后,使用了当前读,下一次事务A使用快照读就会生成新的READVIEW对象,因此会读到脏数据。

用一个例子来演示一下。

使用当前读:
  1. 事务A第一次查询表

image.png 2. 事务B插入一条数据:

image.png

  1. 事务A进行当前读,修改数据(注意,这里必须要影响到新添加的数据,因为这样才会创建新的READVIEW):

image.png

  1. 事务A进行快照读:

image.png

根据上面的操作我们可以发现出现了幻读,表示RR级别的MVCC是解决不了这种情况的幻读的。

必须使用SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE。设置为串行化。 这样在读事务A未提交时添加事务B是执行不了的。


上面内容总结一波:

  1. mysql使用MVCC来解决了脏读,不可重复读,部分的幻读问题。
  2. RC其实是最常用的隔离级别,但是mysql默认是使用的RR隔离级别。
  3. 一般场景下,脏读是无法忍受的,因为会读取到脏数据,不可重复读和幻读一般对业务影响不大,因为他读到的是真实数据。可以根据具体业务来设置隔离级别。