【Mysql】什么是MVCC
基本概念
当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select …for update、update、insert、delete(排他锁)都是一种当前读。
快照读
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
-
Read Committed : 每次select,都生成一个快照读
-
Repeatable Read : 开启事务后,只有第一个select语言会生成快照读,之后的select都会复用快照
-
Serializable : 快照读会退化为当前读
MVCC
Multi-Version Concurrency Control 多版本并发控制
维护一个数据的多个版本,使得读写操作没有冲突。快照读为MVCC提供了一个非阻塞读的功能。MVCC的具体实现,依赖于数据库记录的三个隐藏字段、undo log日志、ReadView。
而给MVCC加上锁,就保证了事务的隔离性
而一致性(事务执行前后,保证数据是一致的),而一致性事由redo log 和 undo log共同来保证的
三个隐藏字段
DB_TRX_ID : 事务ID,记录插入这条记录或最后一次修改记录的事务ID(自增)
DR_ROLL_PTR : 回滚指针,指向这条记录的上一个版本,用于配合undo log
DB_ROW_ID :隐藏主键,如果表结果没有指定主键,则会生成,否则不会
undo log 日志
回滚日志,在insert、update、delete的时候产生,便于数据回滚
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除
而update,delete的时候,产生的undo log日志,需要在回滚和快照读中用到,不会被立即删除
undo log 版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的记录,链表的尾部是最开始的历史记录
ReadView 读视图
快照读,读取的是当前可见记录(可能会有多个历史记录),那么到底读取那条记录,就是由ReadView来决定的,ReadView记录并维护系统当前活跃的事务ID(未提交)
ReadView有四个核心字段:
-
m_ids : 当前活跃的事务ID集合
-
-
max_trx_id : 预分配事务ID,当前最大事务ID+1
-
creator_trx_id : ReadView创建者的事务ID
实现原理:trx_id,当前undo log记录对应的当前事务ID,在决定获取那个历史版本数据时,就是拿trx_id和ReadView中的四个字段属性进行比对,取出匹配成功的记录
多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同事务会话看到自己特定版本的数据,使用版本链。
MVCC只在READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
事务版本号:每开启一个事务,我们都会从数据库中获得一个事务ID(也就是事务版本号),这个事务ID是自增长的,通过ID大小,我们就可以判断事务的时间顺序。
行记录的隐藏列:InnoDB的叶子段存储了数据页,数据页中保存了行记录,而在行记录中有一些重要的隐藏字段,如下所示:
db_row_id:隐藏的行ID,用来生成默认的聚簇索引。如果我们创建数据表的时候没有指定聚簇索引,并且没有唯一索引,这时候InnoDB就会使用这个隐藏ID来创建聚簇索引,采用聚簇索引的方式可以提升数据的查询效率。
db_trx_id : 操作这个数据的事务ID,也就是最后一个对该数据进行插入或更新的事务ID。
db_roll_pointer:回滚指针,也就是指向上一个事务的undo log 日志记录 。这样就形成了版本链。
已提交读和可重复读的区别就在于他们生成ReadView的策略不同
ReadView是如何工作的
在mvcc机制中,多个事务对同一个记录进行更新会产生多个历史快照,这些历史快照保存在Undo log里。如果一个事务要查询这些记录,需要读取哪个版本的历史数据呢?这时就需要用到Read View了,它帮我们解决了行的可见性问题。Read View保存了当前事务开启时所有活跃(还没有提交)的事务列表,换个角度可以理解为Read View保存了不应该让这个事务看到的其他事务ID列表。
在Read View中有几个重要属性
trx_ids:系统当前正在活跃的事务ID集合
low_limit_id:活跃的事务中最大的事务ID
up_limit_id:活跃的事务中最小的事务ID
creator_trx_id:创建这个Read View的事务ID
假设当前有事务creator_trx_id想要读取某行记录,这个行记录的事务id为trx_id,那么会出现以下几种情况。
1、如果 trx_id < 活跃的最小事务ID(up_limit_id),也就是说这个行记录在这些活跃事务创建之前就已经提交了,那么这个行记录对该事务是可见的。
2、如果 trx_id > 活跃的最大事务ID(low_limit_id),这说明该行记录在这些活跃的事务创建之后才创建,那么这个行记录对当前事务不可见。
3、如果 up _limit_id < trx_id < low_limit_id ,说明该行记录所在的事务 trx_id在目前creator_trx_id这个事务创建的时候,可能还处于活跃状态,因此我们需要在trx_ids集合中进行遍历,如果trx_id存在于活跃集合中,证明这个事务还处于活跃状态,不可见。否则,不存在于活跃集合中,说明事务已经提交了,该行记录可见。
了解了这写概念之后,我们看一下当查询一条记录的时候,系统如何通过多版本并发控制技术找到它:
1、首先获取事务自己的版本号,也就是事务ID;
2、获取Read View
3、查询得到的数据,然后与Read View中的事务版本号进行比较。
4、如果不符合Read View规则,就需要从Undo log中获取历史快照
5、最后返回符合规则的数据。
已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的Read View,而可重复读隔离级别则在第一次读的时候生成Read View,之后都复用这个Read View。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY