文章目录
- 前情要点
- 基于什么引擎
- 并发事务产生的问题
- 不可重复读和幻读区别
- Next-Key Lock的示例
- 解决并发事务采用的隔离级别
- 当前读(Current Read)
- 快照读(Snapshot Read)
- 参考
- MVCC
- 定义
- 表里面的隐藏字段
- 由db_roll_ptr串成的版本链
- ReadView
- 可见性算法
- mvcc的可见性算法为什么要以提交的数据为准则?
- RC和RR使用MVCC的不同
- RR级别下是否完全解决幻读问题?
- 不同隔离级别下的实现原理
- 特别鸣谢
前情要点
基于什么引擎
mvcc和事务在mysql中都是基于InnoDB引擎下,抛开这个引擎下谈论就没有意义了,因为并不是所有的引擎都支持事务的,例如MyISAM就不支持。
常见的一些引擎:
并发事务产生的问题
在明白什么是mvcc之前,需要先知道在mysql中两个读的概念;
而在了解什么是当前读和快照读之前,需要先明白在并发事务中常见的问题,如下:
并发事务产生的问题:1.脏读:
A事务对C数据修改,未提交,B事务读取到这个C数据,并使用。这个修改后的C数据是未提交的,属于脏数据。
2.丢失修改:
A事务对C数据修改,修改为C-1;这个时候B事务也对C数据修改,修改成C-1,最终数据修改为C-1,但是实际上正确修改的数据应该为C-1-1。也就是说A事务的修改被丢失了。
3.不可重复读:
A事务第一次读取C数据10之后,B事务修改C数据为20;这个时候A事务第二次读取C数据为20。不可重复读侧重于更新(update),解决不可重复读是加行级锁(所以更多针对的是表中一行数据)。
4.幻读:
A事务第一次读取C数据总数据为10之后,B事务添加到C数据2条新的数据;这个时候A事务第二次读取C数据总数据为12。幻读主要在于添加(insert)和删除(delete),解决幻读是加间隙锁+Next-Key-Lock(所以更多针对的是(范围内的数据集)整张表的数据)
第二点丢失更新属于写写问题,本文不涉及。从这些读取问题中,可以看到如果在使用读取语句的时候不采取任何措施,那么就意味着我们每次读取到数据并不一定是最新,也不一定是已提交的数据。
不可重复读和幻读区别
在网上能够搜到很多文章,都没准确的说明这两个的区别,对于这两个概念一定要理解透,因为对后续理解mvcc很重要。
不可重复读和幻读的区别:不可重复读:多次读取同一行数据,读到的值可能不同(其他事务对共享资源的更新)。
解决不可重复读的主要方法是使用 Repeatable Read 隔离级别。主要涉及行级锁,防止同一行数据在同一事务中多次读取时出现不一致
幻读:多次执行相同的范围查询,结果集中的行数可能不同(其他事务对共享资源的增加和删除)。
解决大部分幻读问题方法涉及间隙锁(Gap Lock)和 Next-Key Lock,防止在范围查询中出现新的行或者被删除了行。完全解决幻读的主要方法是使用 Serializable 隔离级别。
以下来自于chatgpt4.0:
PS: Next-Key Lock:行级锁和间隙锁的组合。它锁定行记录及其前后的间隙,防止在范围查询中出现新的插入或删除操作,从而避免幻读
Next-Key Lock的示例
Next-key Lock的示例如下来自于chatgpt4.0):
以下是关于间隙锁和Next-Key Lock锁的异同(来自于chatgpt4.0):
解决并发事务采用的隔离级别
事务隔离级别:
1.读未提交(READ UNCOMMITTED):允许读取尚未提交的数据,可能导致脏读,幻读,不可重复读,丢失更新。
2.读已提交(READ COMMITTED):读取已经提交的数据,阻止了脏读发生,可能导致幻读、不可重复读,丢失更新。
3.可重复读(REPEATABLE READ):解决了脏读,不可重复读的问题。仍然可能发生幻读,丢失更新。
4.串行化(SERIALIZABLE): 解决了脏读,不可重复读,幻读问题,丢失更新。最高等级,最低效率。。。。事务按照顺序执行。
更详细的事务隔离级别可看这个文章:
结合图文一起搞懂MySQL事务、MVCC、ReadView!
当前读(Current Read)
当前读就是读的是当前时刻已提交的数据。
在mysql中,当前读返回的记录都会加上锁,保证其他事务不会再并发的修改这条记录,也就是说在并发事务中读取到的是已经提交的最新的数据。
例如:
select lock in share mode (共享锁),
select for update; update; insert;
delete (排他锁)
以上操作都是属于当前读。在串行化的事务隔离级别下因为事务都加锁,所以读取到的都属于当前读。
快照读(Snapshot Read)
跟当前读相对应的就是快照读,快照读读取到的不是最新的数据,读取到往往是某个快照版本,而不是当前最新的数据。
在 mysql中,快照读不使用锁,而是通过 MVCC 实现。
例如:
– 快照读示例:普通的 SELECT 语句使用快照读
SELECT column1, column2 FROM table_name WHERE condition;
就是在mysql的默认引擎(InnoDB)的默认隔离级别可重复读(Repeatable Read )下,使用select语句不加锁都是快照读。
参考
【MySQL】MVCC详解与MVCC实现原理(MySQL专栏启动)
当前读与快照读
MVCC
定义
MVCC(Multi-Version Concurrency Control):多版本并发控制。常用于数据库管理系统里面无锁的实现并发控制的方法,同时也是事务隔离级别的一种实现方式,提高了事务的并发性能。(Mysql的innoDB存储引擎使用了。Oracle的undo表空间实现MVCC,PostgreSQL也使用了)
在mysql中读已提交(RC,Read Committed)和可重复读(RR,Repeatable Read)都是通过MVCC实现的。
在了解定义之后,要理解MVCC的实现原理需要需要先理解几个概念。
表里面的隐藏字段
在mysql表里面的行数据中,有三个隐藏的列字段,分别是:db_row_id(唯一行号),db_trx_id(事务id),db_roll_ptr(回滚指针)。
此处主要在于下面两个字段:
db_trx_id:每次⼀个事务对某条聚簇索引记录进⾏改动时,都会把该事务的事务id赋值给trx_id隐藏列
db_roll_ptr:
在undo log日志中存储的每行db_trx_id的上一个版本的回滚指针。每次对某条聚簇索引记录进⾏改动时,都会把旧的版本写⼊到undo⽇志中,然后这个隐藏列就相当于⼀个指针,可以通过它来找到该记录修改前的信息。
ps:
聚簇索引:索引和实际存储的数据在同一个地方,找到索引即找到实际数据值
非聚簇索引::索引和实际存储的数据不在同一个地方,找到索引还需要根据其他索引才能找到实际数据值
以下图片来自于全网最详细MVCC讲解,一篇看懂:
由db_roll_ptr串成的版本链
上面隐藏字段提到的Undo Log,Undo Log会包含每条更新记录的版本信息,包括旧版本的列数据和db_trx_id(事务id)以及db_roll_ptr(指向上一个版本的指针),这样就组成了一个版本链。
下图来自于:MVCC原理 - 我隔壁是老王 - 博客园
不同事务或者相同事务对同一记录行的修改,会使该记录行的 undo log 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录
关于UndoLog日志更为详细的内容,请参考:
【MySql进阶】undo日志详解:undo日志结构、undo日志链表、回滚段、undo log原理
ReadView
一致性视图,全称 Read View ,是用来判断版本链中的哪个版本对当前事务是可见的条件,也就是说判断版本链中哪个版本对当前事务是可以读取到的,包含m_ids,m_creator_trx_id,m_low_limit_id,m_up_limit_id。
下图来自于:全网最详细MVCC讲解,一篇看懂 - 知乎
可见性算法
可见性指的是,当执行一个查询语句(快照读)的时候,当前事务的查询语句可以见到版本链哪条记录。
由隐藏字段db_trx_id(事务id),db_roll_ptr(回滚指针)和Undo Log版本链,Readview就可以实现MVCC了,其对比过程如下:
1.如果被访问版本的 DB_TRX_ID 属性值小于 Read View 中的 m_up_limit_id 值,说明生成该版本的事务在当前事务生成 Read View 之前已经提交,因此该版本可以被当前事务访问。2.如果被访问版本的 DB_TRX_ID 属性值位于 Read View 的 m_up_limit_id 和 m_low_limit_id 之间(包括边界),则需要进一步检查 DB_TRX_ID 是否在m_ids 列表中。如果在列表中,说明在创建ReadView时生成该版本的事务仍处于活跃状态,因此该版本不能被访问;如果不在列表中,说明在创建 Read View 时生成该版本的事务已经提交,因此该版本可以被访问。
3.如果被访问版本的 DB_TRX_ID 属性值大于或等于 Read View 中的 m_low_limit_id 值,说明生成该版本的事务在当前事务生成 Read View 之后才提交,因此该版本不能被当前事务访问。
4.如果被访问版本的 DB_TRX_ID 属性值与 Read View 中的 m_creator_trx_id 值相同,表示当前事务正在访问自己所修改的记录,因此该版本可以被当前事务访问。
mvcc的可见性算法为什么要以提交的数据为准则?
从上面可见性算法的对比过程可以看到,其宗旨都是以已经提交了的数据为其是否可见的判断依据的,那么为什么要以这种已经提交的数据为准?
因为mvcc实现的是在可重复读的隔离级别下保证并发事务的有效进行的方式;在可重复读 的隔离级别下,目的就是为了防止脏读即读未提交,不可重复读的并发问题,所以其准则肯定要是读已提交的事务为准。RC和RR使用MVCC的不同
不同的地方在于可重复读是在第一次查询的时候(快照读)生成的readview,该readview一直被使用,不会生成新的readview;而读已提交则是每次查询的时候(快照读)都生成新的readview从而实现了读已提交。
至于详细的关于其不同的示例,请参考文章中的RC 和 RR 下的 Read View章节:
全网最详细MVCC讲解,一篇看懂
RR级别下是否完全解决幻读问题?
在全网最详细MVCC讲解,一篇看懂 - 知乎我看的这篇文章中,是说防止了部分幻读问题,但是没有完全解决幻读问题。这个结论是对的,但是它的示例却是错误的。。。。
在该文章中关于RR 级别下能否防止幻读两个示例如下:
示例1为:
事务B第一次正常读取(select),而事务B第二次读取使用加锁读取(for update)
示例2为:
事务B第一次正常读取(select),而事务B第二次读取前使用update语句之后再读取;
这两个示例在我第一次读懂之后,也觉得幻读仍然存在,因为按照第一次查询快照读,第二次查询当前读的机制来看,那么也就意味着即使未提交的事务也可以被当前读读取到。因而读取到最新的数据会导致幻读问题。然而,我还是有点疑心,所以就询问了gpt4.0一样的场景,gpt给出的回答是这两个场景仍然不会有幻读问题。问了好几次,才明白过来它为什么这么说。
需要注意的是,上面所展示的事务示例都是在RR的隔离级别下进行示例;
在事务B的第二次读取时,采用加锁查询也好,update之后再读取也罢;都是在两次读取之间采用了当前读和快照读的不同读取方式,而使用第二次读取采用当前读的方式获取最新的数据看起来会致使读取到不同的数据导致幻读,但是忽略了一个点:未提交的事务对于当前事务来说是不可见的。
无论是加锁也好还是MVCC都是为了实现读取到已经提交的事务,来避免出现并发事务的读取问题。
在上面两个示例里面,事务A的插入对于事务B第二次的加锁读取都不是可见的,也就是说事务B压根不知道事务A的插入,因为当前读在mysql中采用的是加锁的方式去获取最新的已提交数据,试问加锁的情况下怎么可能读取到未提交的事务呢?
那么在RR级别下什么时候会出现幻读问题?
在自增列或无索引列的情况下可能导致幻读问题。(来自于chatgpt4.0)这个结论是可以闭环的。还记得前面说到不可重复读和幻读的区别的时候提到的Next-Key Lock和间隙锁吗?这两种锁是实现幻读的基础,如果锁出现问题了那么也就意味着幻读问题可能会发生。
这两种锁都是基于索引,其原因(基于chatgpt4.0)如下:
上图原因总结如下:
1.在无索引的情况下,MySQL 无法有效地使用间隙锁和 Next-Key Lock 来精确锁定范围。只能锁定大范围,不能完全锁住,可能导致幻读问题。
2.自增列的自动递增特性使得未来的插入无法被预锁定。无法预见未来的自增范围,导致新插入的记录不再锁定范围内,只能锁定现有记录和其后的间隙,可能导致幻读问题。
所以可以得出结论如下:
RR级别下没有完全解决幻读问题,当无索引或自增列的情况下会导致间隙锁和Next-Key Lock锁失效的情况,因而会导致幻读问题再发生。
不同隔离级别下的实现原理
不同隔离级别下的锁机制如下:
总结如下:
1.读未提交:不使用锁,无须其他手段;
2.读已提交:使用行级锁;
3.可重复读:MVCC+ 行级锁 + 间隙锁 + Next-Key Lock
3.可重复读:表级锁
特别鸣谢
结合图文一起搞懂MySQL事务、MVCC、ReadView!
全网最详细MVCC讲解,一篇看懂 - 知乎
MVCC原理
MVCC底层实现原理
Chatgpt4.0