MySQL之MVCC

您所在的位置:网站首页 mvcc实现方式 MySQL之MVCC

MySQL之MVCC

2023-07-05 00:19| 来源: 网络整理| 查看: 265

目录 事务什么是事务?事务的四大特性(ACID)事务的四种隔离级别InnoDB中事务回滚的实现方式1. 隐藏字段(回滚指针*DB_ROLL_PTR*)2. undo log MVCC定义及优缺点实现方式:快照读事务隔离级别&快照读个人小总结 幻读问题

一些思考:以下提到的事务、隔离级别、MVCC等概念都是数据库系统的一种规范或思想,思想在前具体实现在后。并不只是Mysql中存在这些理论思想,其他数据库系统中也有,我想表达的是当我们在了解这些概念性的东西时应该注意区分哪些属于思想哪些属于具体的落地实现,这样才会使我们更好的避免局限于某一种技术当中,触类旁通才是更高效的学习方式。

事务 什么是事务? Transaction 直译为 交易、买卖、处理,在数据库中指由一组sql所组成的执行单元(unit);用诸如start transaction和commit语句来界定,语句之间执行的所有操作组成了一个事务。事务具有以下四大特性: 事务的四大特性(ACID) (Aotmatic)原子性:一个事务看做一个最小执行单元,(此事务中的所有sql)要么都成功要么都失败;(Consistent)一致性:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的;(Isolations)隔离性:不同事务间的操作互不影响。即一个事务内部的操作和使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰;(innoDB中由当前读和快照读实现)(Duration)持久性:事务一旦提交,数据将会永久存储到硬盘中;

隔离性关注的点在事务并发时的写写和读写冲突,其中并发写写冲突,InnoDB用加锁来避免,而读写冲突当然也可以直接加锁,但是InnoDB为了考虑并发读写的性能,会根据不同的事务隔离级别 使用不同的并发控制策略(快照度或当前读)。

事务的四种隔离级别

先来看看多个事务之间并发读写时会产生的读问题:脏读、幻读、不可重复读 事务A在修改数据,同时事务B在读取数据

脏读:事务B读取到了事务A修改但还未提交的数据,若此时事务A发生了回滚,那么事务B读取到的这条数据称为脏数据;幻读:指的是事务B前后两次相同的查询操作返回的数据总条数竟然不相同,就像产生了幻觉一样;例如事务B第一次select到了n条数据后,(事务A进行insert/delete操作并提交,)接着事务B再次select时却返回的不是n条;不可重复读:在事务B中,当重复读取某条数据时,每次读取到的结果都可能不一样(即被其他事务并发的修改),那么这条数据是不可重复读的;

事务的四种隔离级别(综合考虑不同的性能和数据隔离需求)一定程度上解决了以上问题:

(Read Uncommitted)读未提交:

顾名思义,当前事务可以读取到其他事务修改了但还未提交的数据。最低级别,没有任何限制,性能最优,但是脏读、幻读、不可重复读都会存在;

(Read Committed)读已提交:

顾名思义,当前事务只会读取到其他事务已经提交了的数据,换言之,未提交的数据读取不到。避免了脏读,但幻读和不可重复读依然存在;

(Read Repeatable)可重复读:

如果当前事务重复读取某条数据时,结果都和第一次读取时相同(即这期间其他事务的修改操作不可见),那么这条数据则是可重复读的。避免了不可重复读和脏读,但幻读依然存在。(其实InnoDB中的快照读机制一定程度上解决了幻读)

例如当前事务读取了id=1的数据,此时其他事务并发的修改id=1的数据不论多少次,当前事务再次读取时结果依然和它第一次读取的结果相同;相当于比“读已提交”更加严格,开启事务后只能读取同一个版本的历史数据,往后就算其他事务提交了修改,当前事务也不会读取到。在InnoDB中由MVCC的快照读实现

(Serialization)串行化:

顾名思义,将并发的事务依次挨个执行(即串行),是最严格的隔离级别。既然是串行的,那就不存在并发问题,但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

大多数数据库默认的事务隔离级别是 Read Committed,比如 SQL Server 、Oracle、Postgres。但 MySQL 的默认隔离级别是 Repeatable Read。

InnoDB中事务回滚的实现方式 两个点:隐藏字段、undo log 1. 隐藏字段(回滚指针DB_ROLL_PTR)

有三个隐藏字段我们需要了解:DB_ROW_ID、DB_ROLL_PTR、DELETE_BIT

DB_ROW_ID

我们都知道在我们创建表时,如果没有主键id字段,那么mysql会自动的为表创建一个隐式id字段作为聚簇索引,此字段被InnoDB取名为DB_ROW_ID;

DB_ROLL_RTX

为了实现事务回滚功能,InnoDB还为表自动创建了另一个隐式字段DB_ROLL_RTX,即回滚指针,此字段指向的是undo log中上一个版本的历史数据;当发生错误需要回滚时,则根据DB_ROLL_PTR 指向的历史数据进行反向更新。

DELETE_BIT

官方文档引用: a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted.

delete操作对内部来说其实是一个update,会将一个特定的bit字段(DELETE_BIT)标记为已删除的状态。后续会有专门的purge线程统一来做删除。

2. undo log

InnoDB将为了回滚而记录的历史版本数据称为undo log。

undo log 主要分为以下三种:

insert undo log: 插入一条记录时,至少把这条记录的主键记录下来,之后回滚的时候只需要把主键对应的记录删除即可。update undo log: 修改一条记录时,至少要把修改这条记录前的旧值都记录下来,在回滚的时候再把这条记录的值更新为旧值就好了。delete undo log: 删除操作只是设置一下老记录的 DELETE_BIT,并不是真正将其删除,后续会有专门的purge线程统一来做删除。(这里还有疑惑?到底是标记此行的DELETE_BIT就行了,还是说会复制整行数据到undo log中呢?) MVCC 定义及优缺点

百度百科:Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问(并发读写)。

MVCC会记录某个时间点上的数据快照。这意味着事务可以看到一个一致的数据视图,不管他们需要跑多久。

这种额外的记录所带来的优点就是对于大多数查询来说根本就不需要获得一个锁,提高了并发读写时的性能(读不阻塞写,写也不阻塞读)。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,做更多的检查工作,处理更多的善后操作。

MVCC只工作在READ COMMITED和REPEATABLE READ隔离级别下。

MVCC不只使用在MySQL中,Oracle、PostgreSQL,以及其他一些数据库系统也同样使用它。

实现方式:快照读

事务间并发时,InnoDB中分别有两种查询数据的方式:当前读和快照读。

当前读 加锁的读(并发会阻塞),针对于写写冲突和读写冲突 时的写,(写其实可以看做先定位到数据然后执行更新操作,所以写是包含了读的过程的)读取到的一定是最新的数据。不同事务之间并发的对同一条数据进行以下操作时,会使用当前读(即加锁的读)

​ update、insert、delete语句和一些特殊的select语句(select ... for share/update)

快照读 不加锁的读(读取快照,并发不阻塞),针对于读写冲突 时的读(普通的select),读取到的不一定是最新的数据(某个时间点的快照),不加锁并发性能好。 当前读的原理是加锁,不是本章重点,这里暂时先跳过,后面有空再整理。快照读的实现原理主要有三个点:隐藏字段、readview、undo log

快照读的实现原理可以看做两部分:历史版本数据+快照(数据权限);

历史版本数据:这点已经由上面提到的undo log实现了;当事务并发时,会产生多个历史版本,那么undo log就会形成一条版本链;

快照(数据权限):隐藏字段TRX_ID+readview;

在innoDB某事务内是在第一次select时触发生成快照信息(数据权限),那么快照信息是个什么结构呢?(TRX_ID+readview)

TRX_ID:

​ 除上面讲回滚指针提到的三个隐藏字段外,还存在第四个隐藏字段TRX_ID,保存的是最后操作这条数据的事务id;执行更新操作时(当前读),都会顺便更新TRX_ID字段为当前事务的id;(更新操作之前的历史数据被存入undo log版本链中)

且事务id是由系统统一生成的全局自增长id,先放一张图

在这里插入图片描述

事务id自增长意味着有时间戳的效果,加上事务id被实时标记在了行数据上(TRX_ID),那么将两个事务id做比较,就可以判断出行数据创建的先后顺序(类似于比较时间戳的大小,TRX_ID值包含了当前行的创建时间信息),这一点下面会用到;

readview:

​ 如果在执行图中的第三个事务时(事务id=13),进行select(第一次快照读)操作,则会触发readview的生成,readview中主要的字段如下:

trx_ids:readview创建时系统中活跃的(**还未提交的)**事务id集合,包括当前事务id;

up_limit_id:trx_ids中最小的id;

low_limit_id:trx_ids中最大的id的下一个id,此时这个id还尚未分配;

当进行快照读时select * from user where id=1,

首先定位到id=1这行数据,然后根据当前行数据的TRX_ID 和 readview来综合判断 这个最新版数据是否对当前事务可见,如果不可见则取出前一个版本再次判断,如此循环判断直到满足条件为止,而后返回这条判断通过的历史数据。

判断可见 的主要逻辑如下:

if 当前行数据的TRX_ID < up_limit_id?,TRX_ID小于up_limit_id 表示 当前行数据是在readview生成之前创建的且已提交的数据,返回true可见;

推导过程:当前行的TRX_ID小于up_limit_id值 等价于 小于trx_ids集合中最小的id(不在集合范围内),即当前行的TRX_ID在readview生成时属于非活跃的事务(已提交或未创建的事务) ,加上事务id是自增长的,比TRX_ID值大的up_limit_id都是已创建的事务,那么TRX_ID值肯定也是已创建的事务,所以得出结论 当前行是(在readview生成之前就)已提交的数据。

else if 当前行数据的TRX_ID >= low_limit_id?大于等于low_limit_id表示 当前行数据是在readview生成之后产生的数据,相对于readview来说属于不存在的数据,不可见返回false;

else if 当前行数据的TRX_ID包含于 trx_ids中 ?,等价于当前行数据的TRX_ID属于活跃的(还未提交的)事务id 即 当前行未提交,不可读取,反之则反。

源码截图-可见性判断changes_visible方法(来源: MVCC - Noblegasesgoo )

可见性判断

综上可以总结出:快照读时只能读取到相对于readview生成时刻 的最新已提交数据,再往后的更新的数据则不可见

事务隔离级别&快照读 (Read Uncommitted)读未提交:

不论数据是否已提交,对当前事务都可见,直接读取行数据,所以用不上快照读;

(Read Committed)读已提交:

限制了未提交的数据对当前事务不可见,那么如何判断是否提交呢?快照读,因为快照读中的readview保存了(readview生成时刻)未提交的事务信息;

然而Read Committed级别下,是可以实时读取到最新的已提交数据的,不论当前事务运行多久,

但是快照读时只能读取到相对于readview生成时刻 的最新已提交数据,再往后的更新的数据则不可见,

那么如何实现实时读取呢?innoDB中的方式是每次查询触发快照读之前都重新生成readview,这样就能达到读取实时的已提交的数据的效果了;

(Read Repeatable)可重复读:

上面已经说到了:快照读时只能读取到相对于readview生成时刻 的最新已提交数据,再往后的更新的数据则不可见。刚好满足可重复读,

即innoDB只会在首次查询操作时生成readview,之后一直使用这个readview而不会重新生成,以达到可重复读的效果;

官方文档对照:

With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed.

With READ COMMITTED isolation level, the snapshot is reset to the time of each consistent read operation.

​ RR下,快照会在首次查询操作时创建;RC下,快照会在每次查询操作时重置。

Consistent read is the default mode in which InnoDB processes SELECT statements in READ COMMITTED and REPEATABLE READ isolation levels. Because a consistent read does not set any locks on the tables it accesses, other sessions are free to modify those tables while a consistent read is being performed on the table.

​ 快照读是InnoDB在RC和RR隔离级别下处理select语句的默认模式。因为快照读是不加锁的(相对于当前读效率更高),当进行快照读时其他会话可以自由(无阻塞)的进行修改操作。

(Serialization)串行化:

最严格的隔离级别,所有操作都加锁,所以用不上快照读。

个人小总结

读未提交和串行化是两个极端,一个放飞自我毫无顾忌(全不加锁),一个规行矩止不够灵活(全部加锁),都不可取;

而介于两者之间的折中方案尽量考虑了数据正确的同时又兼顾了并发效率,可谓妙哉~

两者中,innoDB中把可重复读作为默认的隔离级别,我想是因为 RR对比RC更严格的同时效率还更高吧(RR下readview只需要生成一次,而RC下每次都需要生成)。

幻读问题

众所周知,RR隔离级别下是避免了幻读问题的,因为快照读时只能读取到相对于readview生成时刻 的最新已提交数据,再往后的新增的数据则不可见。

但实际情况是 快照读一定程度上解决了幻读,并没有完全解决;例如下面的特殊情况:

创建user{id,name}表,先初始化插入两条数据:

idnameTRX_ID1张三102李四10

然后两个事务并发运行:

事务A事务Bstart transaction;(分配到事务id=11)start transaction;(分配到事务id=12)select * from user where id>1; (快照读,生成readview,之后都只能读取此刻的版本数据)insert into user (id,name) values(3,‘王五’); (隐藏字段TRX_ID被设置为12)select * from user where id>1; (再次select时由于是快照读,“王五”这条数据不可见)commit;update user set name=‘赵六’ where id>1;(注意!此时为当前读,能定位到“王五”的行数据,并成功更新,且TRX_ID也被覆盖为当前事务id11)select * from user where id>1; (快照读,但是由于“王五”这条数据的TRX_ID被更新为了当前事务id11,所以数据行可见,产生了幻读!)…commit;

如果你很不幸,遇到了上述情况,解决办法是:

避免快照读转而使用当前读select ... for update/share,因为这种方式会加锁(next-key lock),保证区间范围内的数据在当前事务提交前不会被其他事务修改;

下一篇:mysql中的锁…



【本文地址】


今日新闻


推荐新闻


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