MySQL MVCC是什么

57 阅读13分钟
  1. 什么是事务?

事务是数据库系统中执行的一个逻辑操作单元,用于保证数据的一致性。在同组事务中的所有操作,要么同时成功,要么同时失败。不存在部分操作成功部分操作失败的情况。

为什么需要事务?

在用户A给用户B进行转账的流程中(用户A的余额充足的情况下),一般会执行下面的步骤:

  1. 扣减用户A的余额
  2. 增加用户B的余额

如果在执行完成步骤一后再执行步骤时,由于sql语句错误或是数据库由于某些的原因宕机,导致步骤二没有正常执行,此时我们就会发现,用户A的余额被扣减但用户B的余额却没有增加,这显然不符合数据的一致性。

如果使用事务来执行这两个的操作的话,有任意一个的操作失败事务都会回滚(RollBack),数据回到事务开启前的状态,只有当事务中所有的操作都执行成功后事务才会被提交(commit),数据才能被正常的保存。

事务的四大特性(ACID)

原子性(Atomic)

即同组事务中的所有操作要么同时成功要么同时失败,不存在部分操作成功部分操作失败的情况。如果事务中的某个操作失败,数据库中的数据就会回滚到事务执行前的状态。

一致性(Consistency)

事务执行前后数据满足完整性约束,数据库保持一致性状态。即不会发生上面这种用户A给用户B转账,用户A扣减了余额但用户B余额没有增加的情况。

隔离性(Isolation)

数据库允许多个并发事务对数据库进行读写操作,但是每个事务中的操作都是独立于其他事务的,即不会出现并发事务将相互干扰导致数据不一致或数据异常的情况。

举个例子:有并发事务A和事务B,现在用户A有余额1000块,事务A和事务B同时进行扣减用户A余额500块的操作。事务A和B同时进行完毕并提交后用户A的余额一定是0而不是其他因为并发问题出现的异常情况。

持久性(Durability)

事务一旦提交,对数据的修改就是永久性的,即使数据库宕机或断电也不会丢失。

Mysql是怎么实现事务的四大特性的?

  • 原子性:undo log 版本链,事务操作失败的话就通过undo log回到事务开启前的版本。
  • 持久性:redo log,redo log是逻辑日志,所有对数据的修改操作都会记录到redo log中持久化存储,断电不丢失
  • 隔离性:MVCC+读写锁
  • 一致性:原子性+持久性+隔离性
  1. 为什么需要MVCC

多事务并发问题

脏读

一个事务读取到了其他事务更改了但是未提交的数据,我们就称发生了脏读现象。

举个例子:

假设我们有一张表记录用户银行账户余额:

CREATE TABLE `bank` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `amount` int DEFAULT NULL COMMENT '账户余额',
  PRIMARY KEY (`id`)
) 

一开始时,用户小明有余额1000,小王有余额1000

此时我们开启一个事务A,查询表中的所有数据:

然后我们在开启另一个事务,开启事务修改小明的账户的余额为500但是不提交(事务隔离级别已经设置为读未提交)

BEGIN
UPDATE bank SET amount=500 WHERE user_name='小明'

在事务A中我们再进行查询:

可以发现事务A读取到的数据和第一次不一样,读取到了事务B未提交的数据,这种就是脏读现象。

幻读

在同一次事务中,多次查询符合条件的数据,出现了前后数据数量不一致的情况。

表中数据:

开启事务A查询余额大于600的数据:

此时开启事务B,向表中插入一条数据但不提交

BEGIN 

INSERT bank VALUES (3,'小林',700)

然后事务A再次查询余额大于600:

此时会发现,事务A查询出来的数据数量从第一次的1条变为了2条。

在同一个事务A中,多次查询余额大于600的数据,数据的数量前后不一致,这就是幻读现象

不可重复读

在同一事务中,多次对同一条数据进行读取,但数据的值出现前后不一致的情况。

表中数据:

我们先开启事务A查询小王的余额:

然后我们开启另一个事务B对小王的余额进行修改但不提交:

BEGIN

UPDATE bank SET amount = 800 WHERE user_name='小王'

此时我们在事务A中再次查询小王的余额:

可以发现,在同一个事务中,对同一条数据(小王余额)的查询出现了前后不一致的情况,这就是不可重复读的现象

事务隔离级别

事务的隔离级别指的是在数据库中,不同事务间相互隔离的程度。

读未提交(Read Uncommited)

最低的事务隔离级别,在当前事务中,可以读取其他未提交的数据。不能避免脏读,幻读,可重复读。

读已提交(Read Commited)

当前事务只能读取其他事务已经提交的数据,避免了脏读,但是幻读和不可重复读还是不能避免。

可重复读(Repeatable Read)

事务执行过程中的数据和这个事务启动所看到的数据一致,因此解决了重复读的问题。但是在读取范围数据时,还是会出现幻读的现象。

可串行化(Serializable)

隔离级别最高,事务在开始执行前加上表级的共享锁,在这个事务级别隔离下不会出现脏读,幻读,可重复读的情况。但是因为加上了表级锁,导致了性能的下降,极大地降低数据库的并发能力。

总结一下:

事务隔离级别\多事务并发问题脏读可重复读幻读
读未提交
读已提交
可重复读
可串行化

为什么要设置不同的隔离级别?

设置不同的隔离级别是为了解决并发事务过程可能出现的各种问题。隔离级别越高,数据库的隔离性当然会越好,出现并发问题的概率就会越小,但同时读写性能也就会相应的下降。设置不同的隔离级别可以方便用户根据业务的实际需求均衡隔离性和性能去选择适合的级别。比如如果是在读多写少的场景下,选择较低级别的隔离级别就完全够用了。

数据库的隔离性可以通过加锁来实现,但是加锁后就会出现比较明显的性能问题。而MVCC就是一种用来解决读写冲突的无锁并发控制,即在不使用锁的情况下满足并发的读写需求。

  1. MVCC是什么

MVCC全称是Multi-Version Coucurrency Control 即为多版本并发控制,是一种用来解决读写冲突的无锁并发控制。使用MVCC可以做到读操作不会阻塞写操作,写操作也不会阻塞读操作,提高了数据库并发读写的性能。

MVCC的实现

通过聚簇索引中的两个隐藏字段,ReadView和Undo log实现。

聚簇索引中的两个隐藏字段

image.png 对于Innodb存储引擎中的数据表,它的聚簇索引中通常会包含下面两个隐藏的字段:

  • roll_pointer:当对这条记录进行修改时,旧版本就会记录到undo log中,roll_pointer执行的就是上一个版本的旧记录。
  • trx_id:存储的是上一个对这条聚簇索引进行修改的事务ID。

UndoLog

Undo log是一种逻辑日志,记录的是数据修改前的旧版本行数据。

数据被修改前,Innodb存储引擎会先将旧记录存储在undo log中,如果事务回滚就可以通过undo log来恢复数据。

image.png

相同事务或同一事务对同一条记录的修改,会导致在undo log中形成一个线性的版本链表,即undo log版本链。

ReadView

ReadView是Innodb执行快照读时产生的一个读视图,里面维护并记录了当前活跃的事务id。Read View主要作用就是用来维护当前事务对不同版本数据的可见性。

Read View中的四个字段

image.png

活跃事务:即已开启但是未提交的事务

  • creator_id:创建该Read View的事务ID

  • m_ids:创建该Read View时所有的活跃事务id列表

  • min_trx_id:活跃事务中最小的事务ID。

  • max_id:创建Read View时当前数据库应该给下一个事务的id,即当前全局最大事务id+1

一个事务去访问数据时,是怎么知道那些数据是可见的呢?

利用Read View中的字段,我们可以对当前的事务做一个划分:

  • 如果当前记录的trx_id<min_trx_id,即修改当前记录的事务ID小于最小活跃事务id的话,说明修改这个记录的事务是在当前的Read View创建之前就已经提交了,所以当前的事务对这条记录是可见的

  • 如果当前记录的min_trx_id<=trx_id<max_trx_id,说明当前修改这个记录的事务在当前的Read View创建之前已开启,对于这条记录的可见性则要看trx_id在不在m_ids中:

    • 如果trx_id在m_ids中,说明修改这个记录的事务仍然在活跃中即这个事务还未提交,所以这条记录对于当前的事务不可见
    • 如果不在m_ids中,则说明修改这个记录的事务已提交,该记录对当前的事务可见
  • 如果当前记录的trx_id>=max_trx_id,则说明修改这个记录的事务是在Read View创建之后开启的,对当前事务不可见。

  • 如果当前记录的trx_id=creator_id,则说明修改这个记录的事务就是当前事务,对当前事务可见

可重复读隔离级别下MVCC的实现

在事务开启时创建一个Read View,后面在整个事务期间都使用这个Read View。

现在有数据:它的trx_id是10

image.png

我们先后两个事务A和事务B,它们的trx_id分别为11和12,则它们的Read View元数据如下:

image.png

  • 事务B读取id为1的数据,可以读取到amount=1000
  • 事务A对id为1的数据进行修改,将amount修改成500,此时事务A不提交,生成的版本链如下:

image.png

  • 事务B对id为1的数据再次进行读取,对于trx=11的记录,因为此时 min_trx_id<=trx_id<max_trx_id且trx_id在m_ids列表当中,说明修改这条记录的事务还未提交,所以这条记录对事务B不可见。此时事务B会沿着版本链找到上一个版本,再次判断这条数据是否可见,此时trx_id=10,trx_id<min_trx_id,说明修改这条记录的事务已提交,这条记录对事务B可见,事务B就会展示这条记录,此时事务B读取到的amount=1000
  • 事务A进行提交
  • 事务B再次对id为1的数据进行读取,在可重复读的隔离级别下,事务B会使用第一次创建的Read View,所以对最新数据的可见性判定和上次一样,读取到的是amount=1000的数据。

读已提交隔离级别下MVCC的实现

每次读取数据时创建一个Read View。

同样使用上面的例子:

  • 事务B读取id为1的数据,此时事务B创建一个Read View:

image.png

  • 此时事务A将id为1的amount修改为500,这条记录的版本链还是和上次的一样:

image.png

  • 事务B再次读取id为1的数据,因为此时的创建的Read View和第一次读取的一样,所以此时事务B对这条数据的可见性和在可重复读隔离级别下的一致,读取到的amount还是为1000
  • 事务A提交
  • 事务B再次读取id为1的数据,此时事务B创建的Read View如下:

image.png

对记录可见性的判定:此时trx_id<min_trx_id,所以此时第一条记录对事务B可见。

所以此时事务B读取到id为1的amount为500。

  1. MVCC能解决幻读吗

虽然在可重复隔离级别下,幻读是不可避免的,但是MVCC可以在一定的场景下解决的幻读,不过并没有完全的解决。

MVCC是怎么解决幻读的?

同样使用上面的例子:

image.png 创建事务A和事务B,事务A和事务B的Read View和上面的一样:

image.png

此时事务A查询amount>500的数据:

image.png 然后事务B插入一条数据,然后提交:

image.png

此时事务A再次查询amount>500的数据,对于id为1的数据,事务A肯定是可以查出来的,但是对于id=2的数据,因为id=2的数据的trx_id=12=max_trx_id,说明这条记录是在Read View创建后开启的,对于事务A不可见。所以事务A查询结果和第一次一样,只能查到id=1的记录,没有出现幻读的现象。

MVCC不能避免幻读的场景

当前有数据:

分别开启事务A和事务B

事务A执行一次查询amount>500的数据:

事务B插入一条数据并提交:

BEGIN
INSERT bank VALUE (4,'abc',666)
COMMIT

然后事务A更新id为4的这条数据


UPDATE bank SET amount=800 WHERE id=4

事务A再次查询amount>500的数据:

此时我们会发现,这次查询出来的数据和第一次的数据数量不一样,出现了幻读的现象。

参考链接:

cloud.tencent.com 看一遍就理解:MVCC原理详解前言 MVCC实现原理是一道非常高频的面试题,最近技术讨论群的小伙伴一直在讨论,趁着国庆节 - 掘金

事务隔离级别是怎么实现的?