MySQL(5)事务原理及MVCC

事务概述

在这里插入图片描述
数据库事务具有ACID四大特性。ACID是以下4个词的缩写:

  • 原子性(atomicity) :事务最小工作单元,要么全成功,要么全失败 。
  • 一致性(consistency): 事务开始和结束后,数据库的完整性不会被破坏 。
  • 隔离性(isolation) :不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)。
  • 持久性(durability) :事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失 。

建表语句

CREATE TABLE t (
	 id INT PRIMARY KEY, 
	 c VARCHAR(100) 
	 ) Engine=InnoDB CHARSET=utf8;
INSERT INTO t VALUES(1, '刘备');

隔离级别

1、未提交读(READ UNCOMMITTED/RU)

脏读:一个事务读取到另一个事务未提交的数据。

如果一个事务读到了另一个未提交事务修改过的数据,那么这种 隔离级别 就称之为 未提交读 (英文名: READ UNCOMMITTED ),示意图如下:

READ UNCOMMITTED 隔离级别示意图
发生时间编号session Asession B
Begin
Begin
Update t Set c = ‘关羽’ where id = 1;
select * from t where id = 1;(此时读到的列c的值为’关羽’)

如上图, Session A 和 Session B 各开启了一个事务, Session B 中的事务先将 id 为 1 的记录的列c 更新为 ‘关羽’ ,然后 Session A 中的事务再去查询这条 id 为 1 的记录,那么在 未提交读 的隔离级别下,查询结果就是 ‘关羽’ ,也就是说某个事务读到了另一个未提交事务修改过的记录。但是如果Session B 中的事务稍后进行了回滚,那么 Session A 中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读,就像这个样子:

READ UNCOMMITTED 隔离级别示意图
发生时间编号session Asession B
Begin
Begin
Update t Set c = ‘关羽’ where id = 1;
select * from t where id = 1;(此时读到的列c的值为’关羽’)
RollBack

脏读 违背了现实世界的业务含义,所以这种 READ UNCOMMITTED 算是十分不安全的一种 隔离级别 。

2、已提交读(READ COMMITTED/RC)

不可重复读:一个事务因读取到另一个事务已提交的update。导致对同一条记录读取两次以上的结果不一致。

如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那么这种 隔离级别 就称之为 已提交读 (英文名: READ COMMITTED ),如图所示:

READ COMMITTED 隔离级别示意图
发生时间编号session Asession B
Begin
Begin
Update t Set c = ‘关羽’ where id = 1;
select * from t where id = 1;(此时读到的列c的值为’刘备)
commit
select * from t where id = 1;(此时读到的列c值为’关羽’)

从图中可以看到,第4步时,由于 Session B 中的事务尚未提交,所以 Session A 中的事务查询得到的结果只是 ‘刘备’ ,而第6步时,由于 Session B 中的事务已经提交,所以 Session B 中的事务查询得到的结果就是 ‘关羽’ 了。对于某个处在在 已提交读 隔离级别下的事务来说,只要其他事务修改了某个数据的值,并且之后提交了,那么该事务就会读到该数据的最新值,比方说:

READ COMMITTED 隔离级别示意图
发生时间编号session Asession B
Begin
select * from t where id = 1;(此时读到的列c的值为’刘备)
Update t Set c = ‘关羽’ where id = 1;(隐式提交)
select * from t where id = 1;(此时读到的列c的值为’关羽’)
Update t Set c = ‘张飞’ where id = 1;(隐式提交)
select * from t where id = 1;(此时读到的列c值为’张飞’)

我们在 Session B 中提交了几个隐式事务,这些事务都修改了 id 为 1 的记录的列c的值,每次事务提交之后, Session A中的事务都可以查看到最新的值。这种现象也被称之为不可重复读

3、可重复读(REPEATABLE READ/RR)

一般数据库的RR级别下,会出现幻读。只能通过Serializable隔离级别才能去避免幻读。
但是MySQL中的RR级别下,可以解决幻读问题。

幻读:一个事务因读取到另一个事务已提交的insert数据或者delete数据。导致对同一张表读取两次以上的结果不一致。

在一些业务场景中,一个事务只能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。那么这种 隔离级别 就称之为 可重复读 (英文名: REPEATABLE READ ),如图所示:

REPEATABLE READ 隔离级别示意图
发生时间编号session Asession B
Begin
select * from t where id = 1;(此时读到的列c的值为’刘备)
Update t Set c = ‘关羽’ where id = 1;(隐式提交)
select * from t where id = 1;(此时读到的列c的值为’刘备’)
Update t Set c = ‘张飞’ where id = 1;(隐式提交)
select * from t where id = 1;(此时读到的列c值为’刘备’)

从图中可以看出来, Session A 中的事务在第一次读取 id 为 1 的记录时,列 c 的值为 ‘刘备’,之后虽然 Session B 中隐式提交了多个事务,每个事务都修改了这条记录,但是 Session A 中的事务读到的列 c 的值仍为 ‘刘备’ ,与第一次读取的值是相同的

4、串行化(SERIALIZABLE)

以上3种隔离级别都允许对同一条记录进行 读-读 、 读-写 、 写-读 的并发操作,如果我们不允许 读-写 、 写-读 的并发操作,可以使用 SERIALIZABLE 隔离级别,示意图如下:

SERIALIZABLE隔离级别示意图
发生时间编号session Asession B
Begin
Begin
Update t Set c = ‘关羽’ where id = 1;
select * from t where id = 1;(等待中)
commit
select * from t where id = 1;(此时读到的列c值为’关羽’)

如图所示,当 Session B 中的事务更新了 id 为 1 的记录后,之后 Session A 中的事务再去访问这条记录时就被卡住了,直到 Session B 中的事务提交之后, Session A 中的事务才可以获取到查询结果。

5、丢失更新
在这里插入图片描述
两个事务针对同一数据都发生修改操作时,会存在丢失更新问题。

6、总结

  • 数据库的事务并发问题需要使用并发控制机制去解决,数据库的并发控制机制有很多,最为常见的就是锁机制。锁机制一般会给竞争资源加锁,阻塞读或者写操作来解决事务之间的竞争条件,最终保证事务的可串行化。
  • 而MVCC则引入了另外一种并发控制,它让读写操作互不阻塞,每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回,由此解决了事务的竞争条件。

版本链

1、回滚段/uodo log

根据行为的不同,undo log分为两种: insert undo log 和 update undo log

  • insert undo log:

是在 insert 操作中产生的 undo log。 因为 insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作

在这里插入图片描述

  • update undo log :

是 update 或 delete 操作中产生的 undo log。 因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此 update undo log 不能在事务提交时 就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作

如下图所示(第一次修改):
当事务2使用UPDATE语句修改该行数据时,会首先使用排他锁锁定改行,将该行当前的值复制到undo log中,然后再真正地修改当前行的值,最后填写事务ID,使用回滚指针指向undo log中修改前的行。
在这里插入图片描述
当事务3进行修改与事务2的处理过程类似,如下图所示(第二次修改):
在这里插入图片描述
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式。

2、案例
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列( row_id 并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含 row_id 列):

  • trx_id :每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给 trx_id 隐藏列。
  • roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

比方说我们的表 t 现在只包含一条记录:

在这里插入图片描述

假设插入该记录的事务id为 80 ,那么此刻该条记录的示意图如下所示:
在这里插入图片描述
假设之后两个 id 分别为 100 、 200 的事务对这条记录进行 UPDATE 操作,操作流程如下:

发生时间编号trx_100trx_200
Begin
Begin
Update t Set c = ‘关羽’ where id = 1;
Update t Set c = ‘张飞’ where id = 1;
commit
Update t Set c = ‘赵云’ where id = 1;
Update t Set c = ‘诸葛亮’ where id = 1;
commit

每次对记录进行改动,都会记录一条 undo日志 ,每条 undo日志 也都有一个 roll_pointer 属性( INSERT 操作对应的 undo日志 没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
在这里插入图片描述
对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链 ,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个信息很重要,我们稍后就会用到。

ReadView

1、ReadView概述

  • 对于使用 READ UNCOMMITTED 隔离级别的事务来说,直接读取记录的最新版本就好了。
  • 对于使用 SERIALIZABLE 隔离级别的事务来说,使用加锁的方式来访问记录。
  • 对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,就需要用到我们上边所说的版本链 了。
  • 核心问题就是:
    需要判断一下版本链中的哪个版本是当前事务可见的。所以设计InnoDB 的设计者提出了一个ReadView的概念,这个 ReadView 中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。
  • Readview 是为了去查找会话中应该显示版本链中的哪一个版本的数据

2、数据版本可见性判断标准:

  • 如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id,表明生成该版本的事务在生成ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id,表明生成该版本的事务在生成ReadView 后才生成,所以该版本不可以被当前事务访问。
  • 如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
    在这里插入图片描述

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。

什么时候产生Readview?

  • 1、只有select语句才会产生Readview
  • 2、RC级别下,事务内的每一个select语句都会产生一个Readview
  • 2、RR级别下,事务内的第一个select语句才会产生一各Readview,事务内后面的select语句不会产生Readview

事务ID什么时候产生呢?

  • 事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。

在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView 的时机不同,我们来看一下。

3、READ COMMITTED 隔离级别下的Readview案例说明

每次读取数据前都生成一个ReadView

比方说现在系统里有两个 id 分别为 100 、 200 的事务在执行:

# Transaction 100 
BEGIN; 
UPDATE t SET c = '关羽' WHERE id = 1; 
UPDATE t SET c = '张飞' WHERE id = 1;
# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
...

此刻,表 t 中 id 为 1 的记录得到的版本链表如下所示:
在这里插入图片描述
假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200未提交 
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备’

这个 SELECT1 的执行过程如下:

  • 在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [100, 200] 。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 c 的内容是 ‘张飞’ ,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘关羽’ ,该版本的 trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘刘备’ ,该版本的 trx_id 值为 80 ,小于m_ids 列表中最小的事务id 100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 c 为 ‘刘备’ 的记录。

之后,我们把事务id为 100 的事务提交一下,就像这样:

# Transaction 100 
BEGIN; 
UPDATE t SET c = '关羽' WHERE id = 1; 
UPDATE t SET c = '张飞' WHERE id = 1; 
COMMIT;

然后再到事务id为 200 的事务中更新一下表 t 中 id 为1的记录:

# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
... 
UPDATE t SET c = '赵云' WHERE id = 1; 
UPDATE t SET c = '诸葛亮' WHERE id = 1;

此刻,表 t 中 id 为 1 的记录的版本链就长这样:
在这里插入图片描述
然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个id为 1 的记录,如下:

# 使用READ COMMITTED隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200均未提交 
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备' 
# SELECT2:Transaction 100提交,Transaction 200未提交 
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'张飞'

这个 SELECT2 的执行过程如下:

  • 在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [200] (事务id为 100 的那个事务已经提交了,所以生成快照时就没有它了)。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 c 的内容是 ‘诸葛亮’ ,该版本的 trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘赵云’ ,该版本的 trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘张飞’ ,该版本的 trx_id 值为 100 ,比 m_ids 列表中最小的事务id 200 还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 c 为 ‘张飞’ 的记录。

总结一下就是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

4、REPEATABLE READ 隔离级别下的Readview案例说明

在事务开始后第一次读取数据时生成一个ReadView

比方说现在系统里有两个 id 分别为 100 、 200 的事务在执行:

# Transaction 100 
BEGIN; 
UPDATE t SET c = '关羽' WHERE id = 1; 
UPDATE t SET c = '张飞' WHERE id = 1;
# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
...

此刻,表 t 中 id 为 1 的记录得到的版本链表如下所示:
在这里插入图片描述
假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:

# 使用REPEATABLE READ隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200未提交 
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'

这个 SELECT1 的执行过程如下:

  • 在执行 SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [100, 200] 。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 c 的内容是 ‘张飞’ ,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘关羽’ ,该版本的 trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘刘备’ ,该版本的 trx_id 值为 80 ,小于m_ids 列表中最小的事务id 100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 c 为 ‘刘备’ 的记录。

之后,我们把事务id为 100 的事务提交一下,就像这样:

# Transaction 100 
BEGIN; 
UPDATE t SET c = '关羽' WHERE id = 1; 
UPDATE t SET c = '张飞' WHERE id = 1; 
COMMIT;

然后再到事务id为 200 的事务中更新一下表 t 中 id 为1的记录:

# Transaction 200 
BEGIN; 
# 更新了一些别的表的记录 
... 
UPDATE t SET c = '赵云' WHERE id = 1; 
UPDATE t SET c = '诸葛亮' WHERE id = 1;

此刻,表 t 中 id 为 1 的记录的版本链就长这样:
在这里插入图片描述
然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个id为 1 的记录,如下:

# 使用REPEATABLE READ隔离级别的事务 
BEGIN; 
# SELECT1:Transaction 100、200均未提交 
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备' 
# SELECT2:Transaction 100提交,Transaction 200未提交 
SELECT * FROM t WHERE id = 1; # 得到的列c的值仍为'刘备'

这个 SELECT2 的执行过程如下:

  • 因为之前已经生成过 ReadView 了,所以此时直接复用之前的 ReadView ,之前的 ReadView 中的m_ids 列表就是[100, 200]。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 c 的内容是 ‘诸葛亮’ ,该版本的 trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘赵云’ ,该版本的 trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘张飞’ ,该版本的 trx_id 值为 100 ,而 m_ids 列表中是包含值为100 的事务id的,所以该版本也不符合要求,同理下一个列 c 的内容是 ‘关羽’ 的版本也不符合要求。继续跳到下一个版本。
  • 下一个版本的列 c 的内容是 ‘刘备’ ,该版本的 trx_id 值为 80 , 80 小于 m_ids 列表中最小的事务id 100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 c 为 ‘刘备’ 的记录。

也就是说两次 SELECT 查询得到的结果是重复的,记录的列 c 值都是 ‘刘备’ ,这就是可重复读 的含义。如果我们之后再把事务id为 200 的记录提交了,之后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个id为 1 的记录,得到的结果还是 ‘刘备’

MVCC总结

1、当前读和快照读

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)

  • 快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。
  • 当前读,读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL
InnoDB为例:

  • 快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select * from table where ?;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁
select * from table where ? lock in share mode;//共享锁(S) 
select * from table where ? for update;//排他锁(X) 
insert into table values () ; //排他锁(X) 
update table set ? where ? ; //排他锁(X) 
delete from table where ? ; //排他锁(X)

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁

为什么将 插入/更新/删除 操作,都归为当前读?可以看看下面这个 更新 操作,在数据库中的执行流程:
在这里插入图片描述
从图中,可以看到,一个Update操作的具体流程:

  • (1)当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。
  • (2)待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。
  • (3)一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。
  • (4)根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的, 因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操 作;然后在读取下一条加锁,直至读取完毕。

2、一致性非锁定读

一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制(MVCC)读取当前数据库中行数据的方式。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。
在这里插入图片描述
上图直观地展现了InnoDB一致性非锁定读的机制。之所以称其为非锁定读,是因为不需要等待行上排他锁的释放。快照数据是指该行的之前版本的数据,每行记录可能有多个版本,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,
MVCC)。InnoDB是通过undo log来实现MVCC。

在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB默认使用一致性非锁定读。然而,对于快照数据的定义却不同。在READ COMMITTED事务隔离级别下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,则读取事务开始时的行数据版本。

3、InnoDB的MVCC实现

我们首先来看一下wiki上对MVCC的定义:

Multiversion concurrency control (MCC or MVCC), is a concurrency
control method commonly used by database management systems to provide
concurrent access to the database and in programming languages to
implement transactional memory.

由定义可知,MVCC是用于数据库提供并发访问控制的并发控制技术。与MVCC相对的,是基于锁的并发控制, Lock-Based Concurrency Control

MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。

  • 在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持MVCC。

  • 多版本并发控制仅仅是一种技术概念,并没有统一的实现标准, 其核心理念就是数据快照,不同的事务访问不同版本的数据快照,从而实现不同的事务隔离级别。虽然字面上是说具有多个版本的数据快照,但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB通过事务的undo日志巧妙地实现了多版本的数据快照。

  • 数据库的事务有时需要进行回滚操作,这时就需要对之前的操作进行undo。因此,在对数据进行修改时,InnoDB会产生undo log。当事务需要进行回滚时,InnoDB可以利用这些undo log将数据回滚到修改之前的样子。

  • 从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTD 、 REPEATABLE READ 这两种隔离级别的事务在执行普通的SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。

  • READ COMMITTD 、 REPEATABLE READ 这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同, READ COMMITTD 在每一次进行普通 SELECT 操作都会生成一个 ReadView ,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView ,之后的查询操作都重复这个 ReadView就好了。

总结

MVCC是一种思想,MySQL使用undo log(版本链)和ReadView可见性判断,去实现了MVCC。
MVCC的最大好处就是读不加锁,读写不冲突,同时还能保证事务的隔离性。

4、事务总结

事务的隔离性由多版本控制机制和锁实现,而原子性,持久性和一致性主要是通过redo log、undolog和Force Log at Commit机制机制来完成的。

我们再来总结一下数据库事务的整个流程,如下图所示:
在这里插入图片描述
事务的相关流程说明:

  • 事务进行过程中,每次DML sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页。
  • 然后redo log按照时间或者空间等条件进行落盘,undo log和脏页按照checkpoint进行落盘,落盘后相应的redo log就可以删除了。
  • 此时,事务还未COMMIT,如果发生崩溃,则首先检查checkpoint记录,使用相应的redo log进行数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,然后就使用undo log进行事务回滚。
  • 事务执行COMMIT操作时,会将本事务相关的所有redo log都进行落盘,只有所有redo log落盘成功,才算COMMIT成功。然后内存中的数据脏页继续按照checkpoint进行落盘。如果此时发生了崩溃,则只使用redo log恢复数据
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值