深度理解数据库的脏读、脏写、幻读、不可重复读

我们都知道 MySQL 是支持多事务并发执行的,否则一个事务一个事务串行化处理,用户都要砸键盘了。那么,多个事务同时写一行数据怎么处理?一个事务在写数据的时候,另一个事务要读,又该怎么处理这个冲突?为了解决这些问题,MySQL 使用了 MVCC 多版本控制机制、事务隔离机制、锁。

最耳熟能详的就是,事务可以分成 4 个隔离级别:读未提交、读已提交、可重复读、串行化。用的最多的就是 InnoDB 默认的隔离级别——是可重复读 REPEATABLE READ,一般会叫它的缩写「RR」。

说到这里,就会产生几个疑问:

  • 我们知道有事务隔离这回事儿,那为什么要隔离?为什么隔离还不够,还要分级?
  • 事务隔离级别解决了什么问题?没有解决什么问题?
  • MySQL 是如何实现这几个隔离级别的呢?它们底层的工作原理是什么呢?
多个事务并发执行,是怎么一个场景?

不知道你第一次听到「事务隔离机制」的时候是怎么想的,我的第一反应就是:好好的事务,为什么要给它隔离呢??

多个事务并发执行的场景是这样的。我们有一个业务系统,里面很多线程在执行业务代码,比如说 Service 去调用 Dao,Dao 去操作数据库。在业务代码层面,对数据库的操作我们可能会给它加上事务,加上事务之后,如果执行成功就 commit,执行失败就要 rollback。

上面这个流程大家肯定很熟了吧。接下来,由于 MySQL 中的数据是保存在磁盘上。但你要知道,随机读磁盘是很耗时间的,对于频繁的 IO 操作,通常的做法就是先把数据加载到内存里面。MySQL 中就有个内存组件 Buffer Pool,执行增删改查的时候,都会把数据从磁盘加载到 Buffer Pool 中,再执行增删改查操作。

现在要来操作内存(Buffer Pool)中的数据了,由于 MySQL 要支持事务,它是通过什么实现事务的呢?这就涉及到 undo log、redo log来支持 rollback 和 commit 操作了。

上面讲的是一个事务执行的大致流程,那假设这里的每个线程都开启一个事务,那此时就是多个事务并发执行的场景了。如下图所示:

现在你知道多个事务并发执行是怎么一回事儿了,那又有第二个问题了:多个事务并发又咋了,有什么问题么?又没多吃你家一块肉!实际上这里是有问题,因为允许多个事务并发执行,那它们就可能同时访问同一行数据,这就会发生并发问题了。

  • 写冲突,多个事务同时对缓存页里面的一行数据进行更新,允许谁来写?这个冲突要怎么解决?可不可以用锁来解决?
  • 读写冲突,一个事务在写数据,别的事务过来读数据了,这个时候要怎么办?

MySQL 解决它们的方法就是 MVCC 多版本控制机制、事务隔离级别、锁机制。

现在你已经知道了多个事务并发执行是怎么样的一个场景,也知道这样会产生各种冲突。有事务在写数据的时候,别的事务要读同一行的数据那怎么办?一个事务写到一半反悔了,要回滚又会产生什么问题?

这种读写冲突可能导致的问题,前辈都帮我们总结好了,就是脏写、脏读、不可重复读幻读的问题。

脏写

MySQL 的数据是放在一个个缓存页里面的,然后每个缓存页里面是一行行的数据,就像下面这张图这样:

现在有一个事务 A 正在执行,它执行的是一个写操作,原来有一行数据是 NULL,在它执行了 update 操作,把 NULL 改成了值 A,就像下面这张图这样:

注意,这里事务 A 更新了一行数据但是它并没有提交。紧接着事务 B 也来写这行数据了,这就是多个事务并发执行,还操作同一行数据的场景了。事务 B 做的也是 update 操作,把值 A 改成了值 B,如下图所示:

前面说了事务 A 此时是没有提交的,除了提交事务,还可以干嘛?对了,事务 A 是可以回滚的!回滚意味着什么?回滚是不是就意味着原来那一行数据,要回滚到事务 A 执行之前的那个值,也就是 NULL:

事务 A 回滚了,对于事务 B 意味着什么?事务 B 明明正常写了一行数据,但是写完之后发现值变了,变成一个莫名其妙的值。

这就是脏写,脏写就是说我两个事务来写同一行数据,但是前面的那个事务反悔了,回滚了。在后面的事务 B 眼里,我明明修改了数据,怎么会写错呢?它打算也想不到,是别的事务回滚了。

脏读

脏读的情况和脏写差不多:

  1. 事务 A 先写数据,把一行数据的值从 null 改成了 A,同样事务 A 并没有提交;
  2. 然后事务 B 过来读了,它读到的值自然是 A 喽;
  3. 接着事务 A 又回滚了!回滚之后值就要从 A 变回到 NULL;
  4. 事务 B 再去读的时候读到的就是 NULL 了

脏读就是事务 B 因为事务 A 回滚,读不到之前的值了。

幻读

先看图

幻读是指查到了之前没有的一批数据:

  1. 事务 A 里有一个条件查询的语句 select name from t where id > 10,它进行了一次范围查询,查到了 10 行数据;
  2. 然后事务 B 网里面加入了一批数据
  3. 事务 A 再查的用条件查询语句查询的时候,发现查到了 15 条,其中 5 条是之前没见过的。这个事务 A 以为自己出现幻觉了,怎么会多出这么些个数据?这就是幻读了。
不可重复读
不可重复读
  1. 事务 A 先去读一行数据,读到值是 A;
  2. 事务 B 去修改数据,改成了 B。这里和前面不一样的地方就在,事务 B 它还提交了,不回滚了。
  3. 事务 A 第二次去读,读到的是 B,和第一次读到的 A 不一样。

那不可重复读是指什么?它是指在同一个事务里面查询同一行数据,每次查到的数据都不一样。是不是和脏读很像,区别在于脏读是由于别的事务回滚导致,而不可重复读读到的其实是已经提交的数据。

事务隔离机制

前面讲到了,多事务并发执行是会带来脏写、脏读、幻读和不可重复读的问题,MySQL 是如何解决这个问题的呢?答案其实每个人都听过,就是使用事务隔离机制,包括 read uncommitted(读未提交),read committed(读已提交),repeatable read(可重复读),serializable(串行化)这几个隔离级别。

我们回过头来看脏写和脏读。它们其实都是后面的事务正常执行,但是前面的事务回滚了导致的。这个时候它们就是处在 read uncommitted(读未提交)这个隔离级别之下,能够读到别人没有提交的事务。

那么现在提高事务的隔离级别,变成 read committed(读已提交)会怎么样呢?比如说脏读,事务 A 修改了值,从 NULL 变成了值 A。这个时候事务 B 来读,由于隔离级别是 RC,事务 A 没有提交事务的情况下,事务 B 是读不到的,也就不存在事务 A 回滚导致事务 B 第二次读到值和第一次不一样了。

不可重复读和幻读则与脏写和脏读有所区别了,它们读到的都是已经提交了的事务。但是在 repeatable read(可重复读)这个隔离级别中,就能够做到事务开启之后,不论别的事务是否提交,它读到的值都和最开始一样。这就要基于 MVCC 多版本控制机制来讲了,在这里先卖个关子,后面会专门写一篇文章来讲 MySQL 的 RR 是如何实现的。

最后就是 serializable(串行化),串行化很好理解,就是一次只能执行一个事务,完全禁止并发,大家排好队一个个执行,啥事儿都没有喽。但实际开发中是不会有人用这个隔离级别的,重点再念一遍「RR」repeatable read(可重复读)。

结语

先回顾一下,这篇文章先讲了多事务并发执行的场景是怎么样的?紧接着引出了多事务并发可能带来的问题,其中就包括脏写、脏读、不可重复读和幻读。最后介绍了一个事务的隔离级别:read uncommitted(读未提交),read committed(读已提交),repeatable read(可重复读),serializable(串行化),以及它们是如何解决脏写、脏读、不可重复读和幻读问题的。

文章中还留下一个小坑没有填,那就是 MySQL 中最常用到的隔离级别可重复读是怎么实现,MVCC 版本控制机制是到底是什么意思。

发表评论

邮箱地址不会被公开。 必填项已用*标注