面试官问我MVCC,我笑了

数据库 MySQL
mysql的默认存储引擎是innodb,该引擎是默认支持事务以及事务的回滚,事务就是通过各种读写锁来实现的,那么读写锁就涉及到读锁和写锁之间的冲突.

面试官:平时用的数据库有哪些呢

表妹:亲爱

mysql的默认存储引擎是innodb,该引擎是默认支持事务以及事务的回滚

事务就是通过各种读写锁来实现的,那么读写锁就涉及到读锁和写锁之间的冲突

而innodb为了提高读取的效率,增加了MVCC多版本并发控制来更高效率的支持mysql中的读取

事务

SQL语言共分为四大类

数据查询语言DQL,数据操纵语言DML,数据定义语言DDL,数据控制语言DCL。

1. 数据查询语言DQL:数据查询语言DQL基本结构是由SELECT子句,FROM子句,WHERE

2 .数据操纵语言DML:数据操纵语言DML主要有三种形式,插入,更新,删除。

3. 数据定义语言DDL:数据定义语言DDL用来创建数据库中的各种对象如:表 视图 索引 同义词 簇。DDL操作是隐性提交的,不能rollback

4. 数据控制语言DCL:数据控制语言DCL用来授予或回收访问数据库的某种特权,并控制数据库操纵事务发生的时间及效果,对数据库实行监视等。

事务

事务指的是一组SQL语句,要么全部执行成功,要么全部执行失败,要么提交,要么回滚,这句话大家听得耳朵都长茧子了吧

事务特性ACID

原子性:事务是最小单元,不可再分,要么全部执行成功,要么全部失败回滚。

一致性:一致性是指事务必须使数据库从一个一致的状态变到另外一个一致的状态,也就是执行事务之前和之后的状态都必须处于一致的状态。不一致性包含三点:脏读,不可重复读,幻读

隔离性:隔离性是指当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离

持久性:一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢。

事务隔离级别

未提交读:即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种。

已提交读:即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读

可重复读:读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,

串行化:多个事务时,只有运行完一个事务之后,才能运行其他事务。

隔离级别问题详解

脏读:一个事务处理过程里读取了另一个未提交的事务中的数据

不可重复读:一个事务在它运行期间,两次查找相同的表,出现了不同的数据

幻读:在一个事务中读取到了别的事务插入的数据,导致前后不一致

和不可重复读的区别,这里是新增,不可重复读是更改(或删除)。

这两种情况对策是不一样的,对于不可重复读,只需要采取行级锁防止该记录数据被更改或删除,然而对于幻读必须加表级锁,防止在这个表中新增一条数据。

再议锁和事务问题

相信大家读到这里,应该也大致对锁和事务的关系有了更进一步的理解了吧,不清楚锁的同学赶紧去mysql锁的那一篇看看

来,给大家捋一捋

共享锁,也就是读锁,对一行数据加上共享锁之后,别的事务就无法获得该行数据的排他锁了,别的事务也就暂时无法对这个数据进行修改操作了,也就避免了不可重复读这个问题

排他锁,也就是写锁,一个事务对数据进行修改的时候,就获得相应数据的写锁,这时候别的事务也就无法获得该数据的读锁和写锁了,也就避免了脏读问题

临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

MVCC多版本并发控制

什么是MVCC

全称Multi-Version Concurrency Control,多版本并发控制,属于一种并发控制的手段,一般在数据库管理系统中,实现对数据库的并发访问

数据库就必然涉及到读和写的存在,读写就必然涉及到读写冲突,MVCC在mysql中的innodb引擎实现就是为了更好的解决读写冲突,提高数据库的性能,做到即使有读写冲突的时候,也可以不用加锁的方式,非阻塞方式来实现并发读

最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度

MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而SERIALIZABLE 则会对所有读取的行都加锁

MVCC属于一种悲观锁的实现

当前读和快照读

当前读:像select lock in share mode这是共享锁,select for update , update , insert , delete都是属于排他锁,上面说的采用共享锁和排他锁的这种方式,都是属于当前读,当前读就是读取的记录的最新版本,读取的时候还会保证其他并发事务不会修改当前的记录,会对当前的记录进行加锁,防止修改

快照读:不加锁的正常的select查询都是属于快照读,也就是不加锁的非阻塞读。

当然,快照读的前提是隔离级别不是串行级别,此时便会退化成当前读,之所以出现快照读的情况,是mysql中的innodb引擎基于提高并发性能的考虑,快照读也就是基本多版本的并发控制,来更高效的解决读和写之间的冲突问题

根据业务场景来考虑可以接受的问题,避免了加锁的操作,降低了开销,既然是多版本并发控制,那么就要接受读取到的并不一定是最新版本的历史数据这一场景

实现

MVCC只是一个抽象概念,innodb实现这个靠的是三个隐式字段、undo log日志、Read View来实现的

三个隐式字段

数据库在每行记录中除了记录我们自定义的那些字段之外,还有数据库的隐藏的定义字段,DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID

DB_TRX_ID:最近修改事务ID,也会记录创建这条记录和最后一次修改这个记录的事务ID

DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,存储在undo log日志中的Rollback segment回滚段中

DB_ROW_ID:这个不是一定有,如果表没有创建主键,innodb会自动以这列为主键,以这一列来创建B+树,产生一个聚簇索引,也就是创建的其余索引的B+树的叶子节点存储的是这个主键

实际还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag 变了

再说undo log日志

Undo log日志分为两种insert undo log和update undo log

Insert undo log:这种是事务在insert新数据的时候产生的日志,只有在事务回滚的时候需要,所以在事务commit之后可以立即丢弃该日志

Update undo log:这个是在进行update或者delete而产生的日志,这个不仅是事务回滚的时候需要,在快照读的时候也是需要的,也就是innodb的MVCC机制会用到历史的数据,所以不能随便删除,需要等快照读和事务回滚都不涉及到该日志的时候,这个日志才会被相应的线程统一清楚

Read View

这哥们的作用可以理解为生成的一个镜像数据,记录当时的情况

事务快照是用来存储数据库的事务运行情况。一个事务快照ReadView的创建过程可以概括为:

m_ids:一个数值列表,用于维护 Read View 生成时刻系统正活跃的事务ID列表

up_limit_id:是m_ids活跃事务ID中的最小的事务ID

low_limit_id:ReadView 生成时刻系统尚未分配的下一个事务ID ,也就是目前已出现过的事务ID 的最大值 + 1

可见性比较算法

当事务执行快照读的时候,对该记录创建一个Read View读视图,用于记录此时的情景,把它比做条件用来判断当前事务可以看到哪个版本的数据,到底是看到最新版本,还是看到指向undo log日志中的历史版本呢

我们来一起看可见性算法,来决定该版本是否可见

此图来源于知乎,侵删

https://www.zhihu.com/question/66320138/answer/241418502

算法的流程

1. 当行记录的事务ID小于当前系统的最小活动id,就是可见的。

  1. if (trx_id < view->up_limit_id) { 
  2. return(TRUE); 

2. 当行记录的事务ID大于当前系统的最大活动id,就是不可见的。

  1. if (trx_id >= view->low_limit_id) { 
  2. return(FALSE); 

3. 当行记录的事务ID在活动范围之中时,判断是否在活动链表中,如果在就不可见,如果不在就是可见的。

这里我也别用那些官方语言给大家解释了,我就举个简单的例子给大家解释

滴滴滴,跟上思路,加油,就快结束了

M_ids:一个数值列表,用于维护 Read View 生成时刻系统正活跃的事务ID列表

up_limit_id:是m_ids活跃事务ID中的最小的事务ID

low_limit_id:ReadView 生成时刻系统尚未分配的下一个事务ID ,也就是目前已出现过的事务ID 的最大值 + 1

插入一个记录,事务ID是10,此时版本链是10

执行一个update操作,事务ID是20,此时版本链是20-10,commit

执行一个update操作,事务ID是30,此时版本连是30-20-10,未Commit

执行select,事务ID是40,生成一个ReadView,这是一个镜像,此时可能已经有更多事务操作这条数据了,活跃列表是m_ids是[30],最小事务up_limit_id也是30,最大事务low_limit_id是41

比较过程

按照这个ReadView的事务链30-20-10进行上述算法的比较,30不合适,因为在活跃事务中,20满足条件,所以此时事务ID为40的读取的就是ID为20更新的数据

事务ID30Commit,事务ID50执行update,链变成了50-30-20-10,未提交

关键

此时事务ID为40的再次执行了select操作,查询了该记录

如果事务隔离级别是已提交读隔离级别,这时候会重新生成一个新的ReadView,那此时ReadView已经变了,活跃列表m_ids是[50],最小事务up_limit_id也是50,最大事务low_limit_id是51

于是按照上述比较,30便符合条件了,所以此时读出来的版本就是事务ID30的update数据了

如果事务隔离级别是可重复读,此时不会生成新的ReadView,用的还是开始时候生成的,所以还是20符合条件

两种隔离级别

我们上面说了MVCC只在READ COMMITTED 和REPEATABLE READ 两个隔离级别下工作,已提交读和可重复读的区别在于他们生成ReadView的策略不同

也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView

我们根据名字也可以推断,可重复读,如果每次读取的时候生成新的ReadView了,那符合条件的版本很可能就不一样了,所以查出来的也就不一样了,就不符合条件了,于是用的就是同一个ReadView

 

责任编辑:姜华 来源: Java贼船
相关推荐

2023-12-27 18:16:39

MVCC隔离级别幻读

2024-11-19 15:13:02

2021-12-06 08:30:49

SpringSpring Bean面试题

2021-04-01 08:12:20

zookeeper集群源码

2022-04-10 18:10:24

CURD链表

2022-05-24 08:03:28

InnoDBMySQL数据

2010-08-23 15:06:52

发问

2020-04-16 08:22:11

HTTPS加解密协议

2021-05-20 08:54:16

Go面向对象

2020-12-01 11:50:49

数据库Redis面试

2021-06-03 08:55:54

分布式事务ACID

2020-12-03 07:39:50

HashMap底层数据

2022-04-19 07:31:28

事务隔离机制数据库

2021-11-24 10:10:32

axios前端拦截器

2022-10-17 00:04:30

索引SQL订单

2023-01-03 18:06:42

高并发架构

2021-10-25 08:49:32

索引数据库MySQL

2021-03-11 08:51:00

存储面试位置

2021-04-14 18:58:01

虚拟机 Java内存

2021-08-28 09:06:11

Dubbo架构服务
点赞
收藏

51CTO技术栈公众号