详解MVCC以及尽可能解决幻读的两种方案

MVCC

通过「版本链」来控制并发事务访问同一个记录时的行为

并行事务问题 + 隔离级别

幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

  • 脏读:读到其他事务未提交的数据;

  • 不可重复读:前后读取的数据不一致;

  • 幻读:前后读取的记录数量不一致。

四个隔离级别如下:

  • 读未提交(*read uncommitted*),指一个事务还没提交时,它做的变更就能被其他事务看到;

  • 读提交(*read committed*),指一个事务提交之后,它做的变更才能被其他事务看到;

  • 可重复读(*repeatable read*),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;

  • 串行化(*serializable* );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免). [因为串行化会消耗性能]

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),

解决的方案有两种:

【需要加上next-key-lock,也不能完全避免】Next-Key Locks只能锁住已经存在的范围,无法防止范围外的新插入记录对查询结果产生影响。

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。

  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

这四种隔离级别具体是=如何实现的呢?

  • 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;

  • 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;

  • 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

mvcc和间隙锁解决幻读

mvcc只能快照读下的不可重复读和幻读??

RR情况下一当前读一快照读也会导致不可重复??

全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。

【隔离级别越高,事务并发性会变低,效率也会变低】

开启事务

注意,执行「开始事务」命令,并不意味着启动了事务。在 MySQL 有两种开启事务的命令,分别是:

  • 第一种:begin/start transaction 命令; 【第一条select才开启事务】

  • 第二种:start transaction with consistent snapshot 命令;【马上开启事务】

当前读:

(select ... for update 等语句)

【这是加了共享锁的当前读,可重复读是不加锁的,这里的共享锁不兼容排他锁,另一个事物后续不能再做写操作了】

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:

  • select...lock in share mode(共享锁)。

  • select..…for update、update、insert、delete(排他锁)都是一种当前读。

比如A事务读取不到B事务提交的新数据,因为当前隔离级别是RR(可重复读,不会读取到脏数据,会幻读),加上 select...lock in share mode 就可以读取到(当前读)。-----也就是幻读了

【这里能读到最新的是因为:加了意向共享锁之后会隐式事务提交,也就是里面实际提交了一次事务,而不是什么打破隔离级别啥的】

快照读:

(普通select语句)

【某个时间点数据的快照】

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

  • Read Committed:每次select,都生成一个快照读。

  • Repeatable Read:开启事务后第一个select语句才是快照读的地方。

  • Serializable:快照读会退化为当前读。【每次读取操作都会加速】

InnoDB行格式

InnoDB行格式-COMPACT

记录头信息:delete_mask(标记删除) 、next_record(指向「记录头信息」和「真实数据」之间)、record_type(记录的类型)

  • 设置NOT NULL至少省1字节(以字节为单位分配8位)【也是按照列的顺序逆序排列】

  • InnoDB按道理varchar小于255字节变长字段长度列表1字节,超过的话2字节。

    但是explain的key_len 固定取2字节,毕竟 key_len 的目的只是为了告诉你索引查询中用了哪些索引字段。

变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。

同样的道理, NULL 值列表的信息也需要逆序存放。

MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节

要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535【记录头信息和隐藏字段呢】

行溢出:页的大小一般是 16KB,也就是 16384字节,存不下65532字节,所以会部分存在溢出页,原始的留20字节指向新的数据页(溢出页)

  • Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处 不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。【一个数据页存的下】

MVCC 实现原理:

【作用:快照读时通过mvcc找到对应的版本】 对于delete、update的不会,因为他们是当前读,不经过mvcc,所以才会有RR级别还会有幻读的问题,所以需要begin;之后马上执行当前读来锁住数据,不让其他的插入导致幻读。

【解决不可重复读】

隐藏字段

有三个隐藏(两个或者三个)的字段:

【进行改动时会改变】

undo log回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。【因为插入只有一次,trx_id = 1;】

而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

undo log 版本链:

undo log日志会记录原来的版本的数据,因为是通过undo log 日志进行回滚的。

readview

是快照读SQL执行时MVCC提供数据的依据

当前活跃的事务:未提交

如何确定返回哪一个版本 这是由read view决定返回 undo log 中的哪一个版本。

【creator_trx_id:创建者的事务id(当前事务id?),不是每次快照读就生成一次,RC、RR】

RC隔离级别下,在事务中每一次执行快照读时生成ReadView。 RR隔离级别下,在事务中第一次执行快照读时生成ReadView,后续会复用。 【可重复读,一个事务读取的两条数据应该是一样的】这里是不是有问题??? 第四个条件,并不能读取已提交的事务啊,这不就幻读了?

【所以RC不可重复读就是因为每次生成的readview都是新的,会看到别的事务提交的内容】RR只有事务开始才更新readview,所以别人提交事务也不会更新他的m_ids。

【获取哪个版本的数据:拿undolog的当前事务id和readview的四个字段进行对比】

【m_ids的都是活跃的事务,即未提交】

【RR级别仅在第一次执行快照读生成readview,原来如此,readview的数据在readview被创建后就固定了、不会被更新,除非被新的readview覆盖,难怪会出现条件3的情况(被后来的事务修改并提交事务)

工作原理

读不到时,并不会读取这个版本的记录。而是沿着 undo log 链条往下找旧版本的记录

readview一固定(读到的数据就一样),能够读取的版本链、事务id、他的范围就确定下来了。所以后面再改,也会读到正确的位置。

(1)当前事务id肯定读

(2)当前事务开始之前提交的肯定是要读的。

(3)要看隔离级别,RR肯定不能读。如果是不可重复读可以读取到(每次快照读生成一个readview)【通过readview的生成时期实现】。隔离级别区别不在过滤的条件,而在于它生成的时机

RC:max_trx_id 小于unlog的一条版本,读到的话说明是读已提交。没有读到就是可重复读。【每次快照读刷新因为他要读取最新的数据,要刷新readview】

比如time1、time2,time1读的是A,要想在time2读到最新的数据就不能用RR级别。【需要生成一个全新的readview,因为前面的readview已经固定死了】

规则已经定好了(优雅),事务与事务的区别在于readview什么时候刷新,代码扩展性好

MVCC➕Next-key-Lock 防止幻读

InnoDB存储引擎在 RR 级别下通过 MVCC和 Next-key Lock 来解决幻读问题:两种解决方案尽可能避免幻读(超过间隙锁范围的控制不了):

1、执行普通 select,此时会以 MVCC 快照读的方式读取数据

在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下(如果是update这些当前读就可以读到别人提交的。。)的 “幻读” 【借助第一次快照读时,只生成一次readview】

2、执行 select...for update/lock in share mode、insert、update、delete 等当前读 (MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete)-----【尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。】 -----如果用的普通select不会加next-key-lock所以会出现幻读。

在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB 使用  来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读。【next-key lock 是间隙锁+记录锁的组合】前提事务开启之后就执行当前读

Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁。

比如:

事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事务 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。 这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象。

未完全解决幻读的例子

两个发生幻读场景的例子。

第一个例子:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入(必须commit之后A才能更新这条数据,因为锁??)的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。 ----【先更新了把新数据的trx_id改成事务A的,就可以用B的readview倒反天罡的读取事务A插入的新数据】---不update直接select就不行,因为trx_id事务id还没通过update(当前读)非法更改。

第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读(导致有机可趁),然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。----- 【尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。】 锁住就不能被其他的插入了

注意:

本文章是基于黑马程序员b站网课和小林coding总结出的,在此感谢!

UPDATE 和 DELETE 语句:对于更新和删除操作,InnoDB使用的是当前读(Current Read),而不是一致性读。当前读会读取最新的数据版本,并对读取到的记录加锁,以确保数据的安全性和一致性。因此,UPDATE和DELETE语句不会生成ReadView。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值