数据库隔离级别温故而知新

Sep 26 2019

在工作中,碰到一些问题的时候你会想,诶,这个知识点我似乎没理解透。数据库的隔离级别就是我最近有点困惑的知识点,大家试着回答以下问题:

  1. 数据库的隔离级别究竟是为什么解决什么出题而出现的?

  2. 数据库的隔离级别是怎么实现的?

  3. 数据库的隔离级别跟锁有什么关系,有了数据库的隔离级别,我们平时在写 SQL 的时候还需要手工加锁吗?

  4. 为什么 MySQL 的 MVCC 机制在 RC 隔离级别下没有解决不可重复读的问题?

不断地去思考,不断地去刨根问底,我们才能真正的把一个知识理解透。

数据库的隔离级别

我们先来看一下数据库定义的几种隔离级别,偷个懒直接网上找个图:

isolation-level

我们发现,隔离级别的关键字是“读”。也就是说,隔离级别的作用是为了有效保证并发读取数据的正确性。这就回答了第一个问题。

数据库的隔离级别实现

那么,这几种隔离级别怎么实现的呢?我们可以先试想一下,如果用读写锁来实现,要怎么加锁才能达到效果?

注意:这里是假设用锁来实现,MySQL 的实现原理实际并不是这样的,下面我们会谈到。

脏读问题

事务1更新了一条数据,但是还没提交,此时事务2读取了这条数据,然后事务1因为某些原因进行了回滚,此时事务2读取到的数据其实是不对的,读到了脏数据就称之为脏读。(注:这就是读未提交隔离级别,能读取到未提交的数据,会发生脏读。读未提交不需要解析实现原理了吧,啥都没限制,直接读直接写就是了)

那么,我们怎么避免脏读?

如果用锁来实现的话,我们先来做个约定:更新修改删除数据要加上写锁,读取数据要加上读锁,读锁和写锁是互斥的,读锁和读锁是不互斥的。读锁读完数据就释放,写锁事务提交释放。

有了这个约定,我们就可以这样来避免脏读了:修改数据我们先加上写锁,读取数据要加上读锁,因为事务1还没提交,写锁还没有释放,此时事务2没法加上读锁,这样就读不到数据,避免了脏读。(注:这就是读已提交隔离级别用锁来实现的原理)

不可重复读问题

事务1读取了一条数据,此时事务2更新了这条数据,然后事务1再读取这条数据,发现自己两次读取的内容有点不一样,这就是不可重复读。(注:读已提交隔离级别用锁来实现虽然解决了脏读,但是会存在不可以重复读)

那么,我们怎么避免不可重复读?

我们回想一下发送不可重复读的原因,正是因为我们之前设定读完数据就释放读锁,所以事务2才加上了写锁对数据进行了修改,如果事务1第一次读完数据不释放读锁,事务2就加不上写锁,就不会出现这个问题了。好吧,那读锁也事务提交的时候再释放吧,这样就不会发生不可重复读问题了。(注:这就是可重复读用锁来实现的原理了)

幻读问题

事务1根据查询条件读取了若干条数据,事务2插入了符合事务1查询条件的数据,此时事务1再查一次,发现记录竟然多了一条,这就是幻读。(注:用锁来实现的可重复读虽然解决了不可重复读的问题,但是会存在幻读,因为新插入的数据并没有锁定)

那么,我们怎么避免幻读问题?

既然新插入数据导致我幻读,那我就想办法让他不能插入就行了。我们不仅把符合查询条件的记录锁起来,还把记录前后的 GAP 锁起来,这样其他事务就无法插入新数据了。(注:这就是序列化隔离级别了,你回想一下,这其实就是我在干活的时候,其他事务任何操作都做不了了,只能等我干完了才行)

MVCC

实际上, MySQL 并不会这样实现,因为锁就是性能杀手。在 MySQL InnoDB 存储引擎中,使用的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control,类似我们上面的推导)。

MVCC最大的好处:读不加锁,读写不冲突

注意:在 MySQL InnoDB 存储引擎,MVCC 只用于 RC,RR 两个隔离级别。序列化隔离级别跟我们上面的推导是一样的,读加读锁,写加写锁,读写互斥,性能杀手。

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

那啥是快照读,啥是当前读?以MySQL InnoDB为例:

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

对于 MVCC 的实现原理就不展开分析了,可以参考文末的引用链接。

以上就回答了文章开头的第二个问题。

隔离级别与锁的关系

从上面的分析来看,隔离级别有用到锁。那么我们在平时写业务代码的时候还需要手工加锁吗?

我们试想一个购票的业务方法:

1
2
3
4
{
1. 查询余票(select 余票 from xxxx)
2. 余票扣减(update xxxx set 余票=余票-1)
}

上文谈到,隔离级别是为了在并发的情况下读取数据的正确性。那么现在有两个事务,几乎同时走到了步骤1查询余票(没有显式使用锁是快照度),都还没有走到步骤2。大家查询出的余票都是1,数据正确的。当事务1提交,余票变成0。当事物2提交(没有使用乐观锁),余票变成-1,出问题了,一张票卖了两次。

所以,在这种情况下,我们需要显式使用到锁或者其他方式解决业务并发的问题。隔离级别没法帮我们解决这种并发问题。

MVCC 疑问

既然采用了 MVCC ,那么 RC 隔离级别下读取的应该是快照,为什么没有解决不可重复读呢?因为 RC 在每次读取的时候都会重建 read view ,其他事务修改的数据在第二次读取的时重建 read view 是能读取到的,所以就出现了不可重复读问题。

总结

以上只是我个人不断的自问自答方式来解惑自己的问题,思路有点乱。但是,通过这种不断自问自答的方式刨根问底,很多问题就逐渐明朗起来了。

参考资料