当前位置: 首页 > 科技观察

看一遍就明白了:MVCC原理详解

时间:2023-03-18 23:51:01 科技观察

前言MVCC实现原理是一道非常常见的面试题。最近技术讨论群里的小伙伴们一直在讨论。趁着国庆有空一起聊聊吧。一、相关数据库知识点回顾1.1什么是数据库事务,为什么会有事务transaction?它由有限的数据库操作序列组成。这些操作要么全部执行,要么根本不执行。它是一个不可分割的工作单元。如果A转100元给B,先从A的账户中扣除100元,再向B的账户中添加100元。如果A的100元已经扣了,但是没来得及加给B,银行系统出现异常,最后A的余额减少了,B的余额却没有增加。所以你需要一笔交易,回滚A的钱,就这么简单。为什么会有交易?就是为了保证数据的最终一致性。1.2交易包含哪些特征?事务的四个典型特征是ACID、原子性、一致性、隔离性和持久性。原子性:事务作为一个整体执行,包含在其中的对数据库的操作要么全部执行,要么都不执行。一致性:表示在事务开始前和事务结束后数据都不会被破坏。如果A账户向B账户转10元,无论成功与否,A和B的总金额不变。隔离性:当多个事务并发访问时,事务之间是相互隔离的。一个事务不应被其他事务干扰,多个并发事务应相互隔离。.持久化:表示在事务提交后,事务对数据库所做的操作改变会被持久化到数据库中。1.3事务并发存在的问题事务并发会造成脏读、不可重复读、幻读。1.3.1脏读如果一个事务读取了另一个未提交事务修改的数据,我们称之为脏读现象。假设有两个事务A和B:假设现在A的余额是100,事务A准备查询Jay的余额。交易B先扣除Jay的余额,为10,但还没有提交。A最后读到的余额是90,也就是扣除后的余额。脏读因为事务A读取了事务B未提交的数据,所以这就是脏读。1.3.2不可重复读在同一个事务中,前后多次读取,读取的数据内容不一致。假设有两笔交易A和B:交易A先查询Jay的余额,结果为100,此时交易B扣除Jay的账户余额,扣除10后,提交交易A,然后查看Jay的账户余额,发现:变成了90不可重复读事务A被事务B干扰了!在事务A的范围内,两次相同的查询读取了相同的记录但返回了不同的数据,属于不可重复读。1.3.3幻读如果一个事务首先按照一定的查找条件查询了一些记录,当这个事务没有提交时,另一个事务写入了一些满足那些查找条件的记录(比如insert,delete,update),这就意味着幻读发生。假设有两个事务A和B:事务A首先查询id大于2的账户记录,得到id=2和id=3的两条记录。这时事务B启动,插入一条id=4的记录,提交事务A然后执行同样的查询,但是得到了3条id=2,3,4的记录。幻读事务A查询一个范围的结果集,另一个并发事务B向这个范围插入新数据,并提交事务,然后事务A再次查询同一个范围,但是两次读取的结果集不同,这就是幻读.1.4四大隔离级别为了解决并发事务中的脏读、不可重复读、幻读等问题,UncleDatabase设计了四种隔离级别。它们是未提交读、已提交读、可重复读和可序列化。1.4.1ReadUncommittedReadUncommitted隔离级别只限制不能同时修改两个数据,但是在修改数据时,即使事务没有提交,也可以被其他事务读取到。这种级别的事务隔离存在脏读、重复读、幻读等问题;1.4.2ReadCommittedReadCommitted隔离级别,当前事务只能读取其他事务提交的数据,所以这个事务的隔离级别解决了脏读问题,但是还是会存在重复读和幻读的问题;1.43可重复读可重复读隔离级别限制了读取的数据,不能修改,所以解决了重复读的问题,但是在读取范围数据时,有可能插入数据,所以会出现幻读问题;1.4.4序列化事务的最高隔离级别,在该级别下,所有事务都按序列化顺序执行。所有脏读、不可重复读、幻读的并发问题都可以避免。但是在这种事务隔离级别下,事务的执行是非常消耗性能的。1.4.5四大隔离级别会存在哪些并发问题?隔离级别DirtyreadNon-repeatablereadPhantomreadReaduncommitted√√√Readcommitted×√√Repeatableread××√Serialized×××1.5数据库如何保证事务的隔离性?数据库通过加锁来实现事务的隔离。就好比,如果你想一个人独处,不想被别人打扰,你可以把门锁上。Locking确实好用,而且可以保证隔离。比如序列化隔离级别就是通过加锁来实现的。但是频繁的加锁导致数据读取时无法修改,数据修改时无法读取,大大降低了数据库的性能。那么,如何解决加锁后的性能问题呢?答案是,MVCC多版本并发控制!实现了无锁读数据,同时允许修改读数据。可以边修改数据边读取。2.什么是MVCC?MVCC代表多版本并发控制。它是一种并发控制方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。通俗地说,数据库中同时存在多个版本的数据,不是整个数据库的多个版本,而是某条记录的多个版本同时存在。当一个事务对其进行操作时,需要查看这条记录隐藏列的事务版本id,比较事务id,根据事务隔离级别判断读取哪个版本的数据。数据库隔离级别readcommitted和repeatableread都是基于MVCC实现的。与简单粗暴的加锁方式相比,它使用了更好的方式来处理读写冲突,可以有效提高数据库的并发性能。3.MVCC实现的关键知识点3.1事务版本号每个事务开启前,都会从数据库中获取一个自增的事务ID,从事务ID可以判断事务的执行顺序。这是交易版本号。3.2隐式字段对于InnoDB存储引擎,每行记录都有两个隐藏列trx_id和roll_pointer。如果表中没有主键和非NULL唯一键,就会有第三个隐藏主键列row_id。列名必填说明row_id否单调递增行ID,非必填,占6字节。trx_id是记录数据操作的事务的事务IDroll_pointer是这个隐藏列相当于一个指针,指向回滚段的undolog3.3undologundolog,回滚日志,用来记录信息在修改数据之前。在修改表记录之前,会先将数据复制到undolog中。如果事务回滚,可以通过undolog恢复数据。可以认为,当一条记录被删除时,undolog中会记录一条对应的insert记录,当一条记录被更新时,会记录一条对应的update记录。undolog有什么用?事务回滚时,保证了原子性和一致性。用于MVCC快照读取。3.4版本链当多个事务并行操作一行数据时,不同的事务修改该行数据产生多个版本,然后通过回滚指针(roll_pointer)链接成一个链表。这个链表称为版本链。如下:版本链其实通过版本链,我们可以看到事务版本号、表的隐藏列和undolog之间的关系。我们再分析一下。1)假设现在有一张core_user表,表中有一条数据,id为1,name为孙权:2)现在开始一个事务A:执行updatecore_usersetname="曹操"whereid=1onthecore_usertable,会进行下面的流程操作,首先获取一个事务ID=100,将core_user表修改前的数据复制到undolog中,修改core_user表中的数据,id=1、并改名为曹操,并将修改后的数据事务Id=101改为当前事务版本号,将roll_pointer指向undolog数据地址。3.5快照读取和当前读取快照读取:读取的是记录数据的可见版本(有旧版本)。解锁后,普通的select语句都是快照读取,如:select*fromcore_userwhereid>2;当前读取:读取最新版本的记录数据,显式锁定为当前读取select*fromcore_userwhereid>2forupdate;select*fromaccountwhereid>2lockinsharemode;3.6阅读视图什么是阅读视图?是事务执行SQL语句时产生的读视图。事实上,在innodb中,每条SQL语句在执行前都会得到一个ReadView。阅读视图有什么用?主要用于可见性判断,即判断当前事务中哪个版本的数据可见~ReadView如何保证可见性判断?我们先看Readviewm_ids的几个重要属性:当前系统中活跃的(未提交的)读写事务ID,它的数据结构是一个List。min_limit_id:表示ReadView生成时,当前系统活跃的读写事务中最小的事务id,即m_ids中的最小值。max_limit_id:表示ReadView产生时系统中下一个事务应该分配的id值。creator_trx_id:创建当前ReadView的事务ID。Readview的匹配条件规则如下:如果数据事务IDtrx_id=max_limit_id,说明生成这个版本的事务是在ReadView生成之后生成的,所以这个版本不能被当前事务访问到。如果min_limit_id=(1)。如果m_ids中包含trx_id,代表ReadView产生的时刻。事务还没有提交,但是如果数据的trx_id等于creator_trx_id,说明数据是自己产生的,所以是可见的。(2)如果m_ids中包含trx_id,且trx_id不等于creator_trx_id,则ReadView生成时,事务没有提交,也不是自己生成的,所以当前事务也是不可见的;(3).如果m_ids中不包含trx_id,说明你的事务在ReadView生成之前已经提交,修改的结果可以被当前事务看到。4.MVCC实现原理分析4.1基于MVCC查询一条记录的流程是什么获取事务本身的版本号,即事务ID获取ReadView查询得到的数据,然后比较阅读视图中的事务版本号。如果不满足ReadView的可见性规则,则需要Undolog中的历史快照;最后,InnoDB返回符合规则的数据。MVCC是通过ReadView+UndoLog来实现的。UndoLog保存历史快照,ReadView可见性规则帮助判断当前版本的数据是否可见。4.2ReadCommitted(RC)隔离级别,不可重复读问题的分析过程创建一张core_user表,插入一条初始化数据,如下:设置隔离级别为ReadCommitted(RC),事务A和事务B执行core_user表同时进行查询和修改操作。事务A:select*fomcore_userwhereid=1事务B:updatecore_usersetname=”CaoCao”执行过程如下:上次事务A查询的结果是name=CaoCao的记录,我们基于MVCC分析执行过程:(1).A要开始交易,首先要获得交易ID100(2)。B发起一个事务,得到一个事务ID101(3)。事务A生成一个ReadView,readview对应的值如下变量值m_ids100,101max_limit_id102min_limit_id100creator_trx_id100然后返回版本链:开始从版本链中选择可见记录:versionchain从图中可以看出,最新版本的columnname的内容是SunQuan,这个版本的trx_id值为100,开始执行readview可见性规则校验:min_limit_id(100)=2的记录。然后启动事务B,插入一条id=5的数据。流程如下:很明显,事务B在执行插入操作的时候,是阻塞的~因为事务A在执行select...lockinsharemode时,不仅给id=3,4的两条记录加了锁(currentread),id>2的范围内也加了一个gaplock。因此,我们可以发现在RR隔离级别下,加锁的select、update、delete等语句会使用gaplock+adjacentkeylock来加锁rangebetweenindexrecordsandavoidinsertingrecordsbetweenrangestoavoidphantoms意思是RR隔离级别解决了幻读问题?4.4.3在这种特殊场景下,貌似出现了幻读的问题。其实在上图中的事务A中,多了一个updateaccountsetbalance=200whereid=5;一步一步的操作,同一个事务,同一个sql,检测到的结果集不一样,这个结果符合幻读的定义~这道题,各位小伙伴,你觉得是幻读问题吗,所以RR隔离级别,还会有幻读问题吗?欢迎大家在评论区留言。参考文献[1]数据库基础(4)InnodbMVCC实现原理:https://zhuanlan.zhihu.com/p/52977862