MySQL 如何解决幻读(MVCC 原理分析)

您所在的位置:网站首页 幻14pcie MySQL 如何解决幻读(MVCC 原理分析)

MySQL 如何解决幻读(MVCC 原理分析)

#MySQL 如何解决幻读(MVCC 原理分析)| 来源: 网络整理| 查看: 265

image-20211214212939553

1. 什么是 MVCC

在之前的文章中详细的介绍了 MySQL 中的事务和隔离级别,在并发访问数据库造成的问题(脏读、不可重复读、幻读),而 MVCC 就是在尽量减少锁使用的情况下高效避免这些问题。

MySQL 四大隔离级别:

隔离级别脏读不可重复读幻读READ UNCOMMITTED:未提交读可能发生可能发生可能发生READ COMMITTED:已提交读解决可能发生可能发生REPEATABLE READ:可重复读解决解决可能发生SERIALIZABLE:可串行化解决解决解决

MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。

同一行数据平时发生读写请求时,会上锁阻塞住。但 MVCC 用更好的方式去处理读写请求,做到在发生读写请求冲突时不用加锁。

这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

那它到底是怎么做到读写不用加锁的,快照读和当前读是指什么?

2. 快照读和当前读

快照读

快照读,读取的是快照数据,不加锁的普通 SELECT 都属于快照读。

SELECT * FROM table WHERE ... 复制代码

当前读

当前读就是读的是最新数据,而不是历史的数据,加锁的 SELECT,或者对数据进行增删改都会进行当前读。

SELECT * FROM table LOCK IN SHARE MODE; SELECT FROM table FOR UPDATE; INSERT INTO table values ... DELETE FROM table WHERE ... UPDATE table SET ... 复制代码 3. 为什么使用 MVCC

在数据库并发场景中,只有读-读之间的操作才可以并发执行,读-写,写-读,写-写操作都要阻塞,这样就会导致 MySQL 的并发性能极差。

采用了 MVCC 机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了 MySQL 的并发性能。

也就是说 MVCC 具体解决了以下问题:

并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。 解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写(需要加锁)问题。 4. MVCC机制的原理

它的实现原理主要是版本链,undo日志 ,Read View来实现的。

4.1 版本链

在之前对 InnoDB 存储引擎的介绍了数据页的行格式,对于使用它的表来说,表中的聚簇索引都包含三个隐藏列:

列名是否必须说明row_id否创建的表中有主键或者非 NULL的 UNIQUE 键时都不会包含 row_id 列trx_id是事务ID,每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列roll_pointer是回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息

现在有这样一张表:

CREATE TABLE `user` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(20) DEFAULT NULL COMMENT '姓名', `sex` char(1) DEFAULT NULL COMMENT '性别', `age` varchar(10) DEFAULT NULL COMMENT '年龄', `url` varchar(40) DEFAULT NULL, PRIMARY KEY (`id`), KEY `suf_index_url` (`name`(3)) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; 复制代码

插入如下一条数据:

INSERT INTO `user` (`id`, `name`, `sex`, `age`, `url`) VALUES ('1', 'ayue', '1', '18', 'https://javatv.net'); 复制代码

假设插入该记录的事务 id 为 60,那么此刻该条记录的示意图如下所示:

image-20211208203939945

假设之后有两个事务 id 分别为 80、120 的事务对这条记录进行 UPDATE 操作,操作流程如下:

Trx 80Trx 120BEGINBEGINUPDATE user SET name = 'a' where id = '1'UPDATE user SET name = 'y' where id = '1'COMMITUPDATE user SET name = 'u' where id = '1'UPDATE user SET name = 'e' where id = '1'COMMIT

每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:

image-20211209091759865

对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。

另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(MVCC) 。

4.2 undo日志

undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。当事务进行回滚时可以通过 undo log 里的日志进行数据还原。(MySQL 中的日志)

Undo log 的用途:

保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。

undo log主要分为两种:

insert undo log

代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃。

update undo log

事务在进行 update 或 delete 时产生的 undo log, 不仅在事务回滚时需要,在快照读时也需要。所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除。

4.3 ReadView

上面说到了,改动的记录都存在在 undo 日志中,那如果一个日志需要查询行记录,需要读取哪个版本的行记录呢?

1️⃣ 对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

2️⃣ 对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录,不存在并发问题。

3️⃣ 而对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。

核心问题就是: READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别在哪里?这两种隔离级别对应的不可重复读与幻读都是指同一个事务在两次读取记录时出现不一致的情况,这两种隔离级别关键是需要判断版本链中的哪个版本是当前事务可见的。

ReadView 就是用来解决这个问题的,可以帮助我们解决可见性问题。 事务进行快照读操作的时候就会产生 Read View,它保存了当前事务开启时所有活跃的事务列表。

注:这里的活跃指的是未提交的事务。

每一个事务在启动时,都会生成一个 ReadView,用来记录一些内容,ReadView 中主要包含 4 个比较重要的属性:

属性说明m_ids生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表min_trx_id生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id 也就是 m_ids 中的最小值max_trx_id生成 ReadView 时系统中应该分配给下一个事务的 id 值creator_trx_id生成该 ReadView 的事务的事务 id,指定当前的 ReadView 属于哪个事务

其中,max_trx_id并不是指m_ids中的最大值,因为事务 id 是递增分配的,假如现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。

再有了 ReadView 之后,在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

trx_id = creator_trx_id ,可访问

如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

trx_id < min_trx_id ,可访问

如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。

trx_id >= max_trx_id ,不可访问

如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。

min_trx_id



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3