文章目录
- 隔离性的进一步理解
- MYSQL的日志
- 隔离性具体是如何实现的呢?
- 回滚操作的具体过程是如何实现的?(MVCC的介绍,什么是MVCC)
- 多次UPDATE操作形成版本链
- 什么是MVCC?
- 听你刚刚的介绍,我每做一次修改(update操作),这次修改前的历史版本就要被拷贝一份放到undo.log中,那我要是修改次数很多,记录的历史版本太多,把undo.log放满了怎么办?
- 一个事务中涉及到了对stu表中张三这条记录的修改操作,请问在这个事务提交之后,undo.log中还保存有张三这条记录的历史版本吗?
- 上面介绍了多次update操作会形成版本链,需要对多版本进行管理和维护,请问delete操作呢?删除一条记录会不会为这条记录形成一个新版本,插入到版本链中?insert操作呢?select操作呢?
- 你隔离性不是也分好几个隔离等级吗?MVCC在哪里体现出了对隔离等级的划分呢?
- 当前读与快照读
- read view介绍
- 什么是read view?
- 如何理解事务ID?事务ID是如何产生的?事务ID的大小有什么含义?
- 事务的readview结构体是在什么时候创建形成的?是事务开始的时候吗?
- readview源码
- 有了readview之后,所有的事务就可以根据readview分成三类,请问是哪三类?
- 当前事务想对一条记录进行查找,我怎么知道到底应不应该让这个事务看到这条记录的最新版本呢?
- 不同隔离级别下ReadView的生成规则
- mysql中如何判断某个事务 ID(id)对应的修改是否对当前事务视图可见?
- 如何理解“某个事务中首次出现快照读的位置,决定该事务后续快照读结果的能力 ”这句话?
- RR和RC在MVCC机制层面的本质区别是什么?
- 并发读写的隔离性是如何实现的呢?
- 推荐阅读
隔离性的进一步理解
数据库并发的场景有哪些?这些并发场景下都存在哪些安全问题?
-
读-读并发
不存在任何问题,也不需要并发控制 -
读-写并发
有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读写
(这个场景其实最难的,也是我们重点谈论的,因为这里面存在 很多可以优化的场景) -
写-写并发
有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
这个场景其实没啥可说的,因为肯定要加锁,所以优化空间不大
为什么事务要有ID?ID有什么作用
ID具有唯一性,用于区分不同的事务
ID的大小用来表明事务的先后顺序
即Mysql按照事务开始的先后顺序分配ID,事务越早开始,ID越小
mysql的表格中有3个记录隐藏列字段 ,分别叫做DB_TRX_ID ,DB_ROLL_PTR ,DB_ROW_ID ,这三个字段表示的含义分别是什么?
DB_TRX_ID :字段长度 6 byte 表示最后一次修改表中这条记录信息的事务ID,如果创建之后没有人改过,那就记录创建这条记录的事务ID
DB_ROLL_PTR :字段长度 7 byte 回滚指针,指向这条记录之前的历史版本
DB_ROW_ID:字段长度 6 byte隐含的自增ID(隐藏主键),如果数据表没有主键,那么DB_ROW_ID就是这张表的主键, InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引 (相当于主键索引)
其实除了上面那三个隐藏列字段之外,还有一个删除flag字段,这个字段的含义是什么?
就是如果你想删除表中的某一行数据,向mysql输入delete操作之后,实际上mysql后台并不会立即把这一行的数据全删了,而是会先将该行的flag字段从1改成0,此后mysql就可以对这块空间进行覆盖写入了
MYSQL的日志
mysql中有三种日志信息:undo log、redo log、bin log,请问这三种日志信息的功能分别是什么?有什么区别?
我们下面就从MYSQL日志系统解决的核心问题入手,来说明三种日志的功能
undo log:解决 “事务原子性” 和 “并发读冲突” 问题
事务原子性问题:如何保证事务执行中途失败(如崩溃、主动回滚)时,能撤销所有已执行的修改,回到事务开始前的状态?
undo log记录 “反向操作” 实现回滚:
事务执行时,每做一次修改(如 INSERT/UPDATE/DELETE),undo log 就记录对应的 “撤销指令”:
- 若执行 INSERT,则记录 DELETE(回滚时删除新增的行);
- 若执行 UPDATE,则记录 “旧值”(回滚时将字段恢复为修改前的值);
- 若执行 DELETE,则记录 INSERT(回滚时重新插入被删除的行)。
当事务需要回滚时,InnoDB 会反向执行 undo log 中的指令,逐步撤销所有修改。
并发读冲突问题:如何让多个事务同时读写数据时,读操作不被写操作阻塞(即 “读不加锁”)?
提供 “历史版本” 支持 MVCC:
- undo log 会保留数据的多个历史版本(按事务顺序生成),每个版本关联对应的事务 ID。
- 当其他事务执行 “快照读”(如普通 SELECT)时,InnoDB 会根据隔离级别,从 undo log 中读取符合条件的历史版本,避免直接读取当前正在修改的数据,实现 “读不加锁” 的并发控制。
在数据库系统中,undo log、redo log、bin log 分别针对三个核心问题设计,各自通过独特的机制解决特定场景的挑战:
redo log:解决“事务持久性”和“写入性能”问题
事务持久性问题
事务持久性问题:如何保证事务提交后,即使数据库崩溃(如断电),修改也不会丢失?
数据库不是存在服务器的磁盘中吗?磁盘是非易失性存储器,不是说非易失性存储器中的数据,即使断电也不会丢吗?你现在咋又说会丢了呢?
这个理解有个关键误区:磁盘的“非易失性”仅保证“已完整写入磁盘的数据不丢失”,但无法解决“数据写入过程中掉电导致的不完整/损坏”,更无法解决“高并发下直接刷数据页的性能灾难”。这正是需要redo log的核心原因——它要解决的不是“已存磁盘数据丢不丢”,而是“如何安全、高效地把内存中的修改落地到磁盘”。
引入redo log之前事务修改数据的完整流程
当mysql服务器收到了客户端的指令,执行update语句修改数据时,数据库的操作不是“直接改磁盘文件”,而是遵循以下步骤(以InnoDB为例):
- 读数据到内存:先从磁盘把目标数据页(默认16KB)加载到内存中的“缓冲池(Buffer Pool)”;
- 内存中修改:在缓冲池里修改数据页的内容(此时内存数据和磁盘数据不一致,称为“脏页”);
- 刷脏页到磁盘:最终需要把缓冲池中的“脏页”写回磁盘,才能让修改永久生效。
为什么“磁盘非易失性”解决不了问题?核心在“写入过程”和“性能”
即使磁盘不会丢已存数据,没有redo log的话,会面临两个致命问题:
问题1:数据页“部分写入”导致损坏,磁盘非易失性救不了
假设一个16KB的数据页正在从内存刷到磁盘:
- 磁盘写入是“按块/按扇区”进行的(比如每个扇区512字节),需要多次IO才能写完16KB的完整数据页;
- 如果刷到第8KB时突然断电,此时磁盘上这个数据页会处于“一半旧数据、一半新数据”的中间状态(即“部分写入”);
- 磁盘的非易失性只能保证“这8KB新数据不会丢”,但无法判断“剩下的8KB为什么没写”——重启后数据库看到这个损坏的数据页,既不能确定它是旧状态还是新状态,也无法修复,最终导致数据损坏。
问题2:直接刷脏页是“随机IO”,高并发下性能崩溃
磁盘的性能瓶颈在于“随机IO”(磁头需要频繁移动定位数据页位置)和“顺序IO”(磁头只需要往后追加写入)的巨大差距:
- 数据页在磁盘上是离散存储的(比如修改用户A的数据在磁盘地址100,修改用户B的数据在地址10000),直接刷脏页就是“随机IO”,每秒只能完成几百次;
- 如果没有redo log,每次事务提交都必须把修改过的脏页“同步刷到磁盘”(否则断电会丢数据)——高并发场景下(比如每秒几千次写入),磁盘IO会瞬间打满,数据库直接卡死。
redo log如何补上这两个漏洞?
redo log日志就是为了解决上述两个问题设计的,本质是用“顺序IO的日志”替代“随机IO的数据页”作为提交的“持久化锚点”:
- 解决“部分写入”问题:记录“修改动作”而非“完整数据”
redo log不记录完整数据页,只记录“某数据页的某位置,从值X改成了值Y”这种“最小修改动作”。
即使数据页刷盘时断电,重启后数据库会:- 先检查磁盘上的数据页状态(无论是否完整);
- 再通过redo log重新执行所有“已提交事务的修改动作”
采用上面的策略,不管数据页之前是旧是新、是否完整,重新应用后都会恢复到正确的最终状态,彻底避免数据损坏。
假如说一个事务要对某16KB大小的数据库进行修改,其中有8处需要修改的数据,4处在前8KB,4处在后8KB。事务提交到服务器之后,服务器开始修改,修改完前面4处,还没修后面4处时,服务器就断电了 ,下次服务器恢复时,他应该怎么办?
假设这16KB数据页的标识为 space_id=1(表空间ID)、page_no=100(页号),8处修改的具体信息如下:
| 序号 | 事务ID | 数据页定位 | 修改位置(偏移量) | 修改内容(旧值→新值) | redo log 记录格式(简化) |
|---|---|---|---|---|---|
| 1 | T100 | space=1, page=100 | 0x0020(32字节处) | 0x000A(10)→ 0x0014(20) | [T100, space=1, page=100, offset=0x0020, len=4, old=0x000A, new=0x0014] |
| 2 | T100 | space=1, page=100 | 0x0100(256字节处) | 0x000C(12)→ 0x001E(30) | [T100, space=1, page=100, offset=0x0100, len=4, old=0x000C, new=0x001E] |
| 3 | T100 | space=1, page=100 | 0x0200(512字节处) | 0x0005(5)→ 0x000F(15) | [T100, space=1, page=100, offset=0x0200, len=4, old=0x0005, new=0x000F] |
| 4 | T100 | space=1, page=100 | 0x1F00(7936字节处,前8KB内) | 0x0030(48)→ 0x0064(100) | [T100, space=1, page=100, offset=0x1F00, len=4, old=0x0030, new=0x0064] |
| 5 | T100 | space=1, page=100 | 0x2000(8192字节处,后8KB起始) | 0x0008(8)→ 0x0010(16) | [T100, space=1, page=100, offset=0x2000, len=4, old=0x0008, new=0x0010] |
| 6 | T100 | space=1, page=100 | 0x3000(12288字节处) | 0x0020(32)→ 0x0040(64) | [T100, space=1, page=100, offset=0x3000, len=4, old=0x0020, new=0x0040] |
| 7 | T100 | space=1, page=100 | 0x3F00(16128字节处) | 0x0050(80)→ 0x00A0(160) | [T100, space=1, page=100, offset=0x3F00, len=4, old=0x0050, new=0x00A0] |
| 8 | T100 | space=1, page=100 | 0x3FF0(16368字节处) | 0x0001(1)→ 0x0002(2) | [T100, space=1, page=100, offset=0x3FF0, len=4, old=0x0001, new=0x0002] |
| 9 | T100 | —— | —— | 事务提交标记 | [T100, COMMIT, xid=12345](事务提交时追加的标记,用于恢复时识别事务已完成) |
当数据库崩溃后重启,会通过这些记录:
- 识别
T100是已提交事务(通过最后一条COMMIT标记); - 按顺序重新应用这8条修改(无论数据页之前是否被部分写入);
- 最终让16KB数据页的8处修改全部生效,与事务提交时的预期完全一致。
我服务器不知道上次断电时这个事务执行到哪了,我就从这个事务的第一条记录开始查,第一条记录要修改的是0x0020这个位置的数据,那我就去数据库中看看这个位置的数据的修改时间是啥时候,如果断电之前修改过,那我就不改了,如果断电之前没改过,我就按照redo log中的修改方式进行修改。后面的记录也是一样,挨个检查是否修改过,断电前没改我就现在改
- 解决“性能崩溃”问题:用“顺序IO日志”替代“随机IO数据页”做提交
redo log文件是固定大小的循环文件,写入时只需从当前位置“追加写入”(顺序IO),性能接近内存(每秒可达百MB级别)。
事务提交时,只需把“修改动作”写入redo log(顺序IO,快),就可以认为事务“已持久化”;而数据页的“脏页刷盘”则交给后台线程异步、批量处理(随机IO,但不阻塞事务提交)。
这样既保证了“事务提交后数据不丢”,又避开了随机IO的性能瓶颈,高并发下也能稳定运行。
总结:磁盘非易失性是“结果保障”,redo log是“过程保障”
- 磁盘的非易失性:负责“一旦数据完整写入磁盘,就永远不丢”——这是“结果层面”的保障;
- redo log:负责“让数据能安全、高效地从内存写到磁盘”——这是“过程层面”的保障(解决写入中掉电损坏、写入性能差的问题)。
没有redo log,磁盘的非易失性就像“有坚固的仓库,却没有安全高效的运输队”——要么运输时货物损坏(部分写入),要么运输太慢导致仓库堵死(性能崩溃)。
redo log 记录“物理修改”实现崩溃恢复:
- 事务执行时,所有对数据页的修改(如某行某字段的值变化)会先记录到
redo log中,内容是“物理地址+修改内容”(例如“在表 t1 的数据页 0x123 中,偏移量 0x45 处的值从 10 改为 20”)。 - 事务提交时,
redo log会被强制刷到mysql服务器磁盘(通过innodb_flush_log_at_trx_commit=1保证)。 - 若数据库崩溃,重启后 InnoDB 会扫描
redo log,重新执行所有“已提交事务”的修改(无论数据页是否刷盘),确保提交的数据不丢失。
写入性能:如何避免每次事务提交都将完整数据页刷到磁盘(成本过高),同时保证数据安全?
这个问题其实上一个问题我们已经讲过了,这里就再提一遍
redo log采用先写日志优化写入性能:
- 磁盘的“顺序写”(如
redo log追加写入)速度远快于“随机写”(如修改数据页时的离散写入)。 redo log采用“循环文件组”设计(固定大小),事务提交时只需顺序写入日志,无需立即刷数据页到磁盘(数据页由后台线程异步刷盘),大幅提升写入性能。
bin log:解决“跨实例数据同步”和“时间点恢复”问题
主从复制问题
如何让从节点数据库同步主节点数据库的所有数据修改,保持主从数据一致?
MySQL 主从复制(Master-Slave Replication)通过 “主库记录变更日志,从库读取并重放日志” 的方式实现数据同步,核心依赖 bin log(二进制日志)和一套协作机制,确保从节点与主节点数据一致。
主从复制分为 3 个关键步骤
1. 主库记录数据变更到 bin log
- 主库开启
bin log功能后,所有对数据的修改操作(INSERT/UPDATE/DELETE等)都会被按顺序记录到bin log中(记录的是逻辑变更,如 SQL 语句或行级修改)。 bin log会为每个事件分配一个 位置编号(Position) 和 日志文件名,用于标记变更的顺序和位置(类似“章节页码”)。
2. 从库获取主库的 bin log
- 从库启动一个 IO 线程,连接主库的专用复制端口(默认 3306),并发送“需要同步的
bin log文件名和位置”(初次同步时从起始位置开始,后续从上次同步的位置继续)。 - 主库接收到请求后,启动一个 dump 线程,将
bin log中“指定位置之后的新变更”按顺序发送给从库的 IO 线程。 - 从库的 IO 线程将接收到的
bin log内容写入本地的 中继日志(Relay Log) 中(中继日志格式与bin log一致,相当于“临时副本”)。
3. 从库重放中继日志中的变更
- 从库启动一个 SQL 线程,读取中继日志中的内容,按顺序解析并执行其中的 SQL 语句(或行级变更),将主库的修改“重演”到从库自己的数据中。
- 执行完成后,从库会记录“已执行到的中继日志位置”,确保下次同步从正确位置继续,避免重复执行。
保证数据一致的关键机制
-
顺序性保证
bin log和中继日志都是 顺序追加写入 的,从库 SQL 线程严格按日志顺序执行,确保与主库的修改顺序一致(避免乱序导致的数据不一致)。
-
断点续传
- 从库会记录“最后一次同步成功的
bin log位置”(存储在master.info文件或系统表中)。即使从库重启,也能从上次中断的位置继续同步,无需重新同步全量数据。
- 从库会记录“最后一次同步成功的
-
事务完整性
- 主库会将整个事务的所有修改作为一个“原子事件”写入
bin log(事务提交后才会记录),从库 SQL 线程会完整执行整个事务的所有操作,保证事务的原子性在从库上同样生效。
- 主库会将整个事务的所有修改作为一个“原子事件”写入
-
存储引擎无关性
bin log是 MySQL 服务器层的日志(非引擎层),无论主库使用 InnoDB、MyISAM 还是其他引擎,所有数据变更都会被记录,确保从库能同步所有引擎的修改。
常见的同步架构(优化与扩展)
- 一主多从:一个主库可对应多个从库,分担读压力(主库写,从库读)。
- 级联复制:从库同时作为其他从库的“主库”,减少主库的 dump 线程压力。
- 半同步复制:主库提交事务后,需等待至少一个从库确认“已接收
bin log”才返回成功,避免主库崩溃后数据丢失(默认是异步复制,主库不等待从库)。
MySQL 主从复制的核心是 “主库写 bin log,从库 IO 线程传日志,SQL 线程重放日志”,通过日志的顺序性、事务的原子性和断点续传机制,确保从库数据与主库一致。这种机制既实现了数据备份,又能通过“读写分离”提升数据库的并发处理能力。
时间点恢复问题
如何在数据库全量备份后,恢复“备份之后”的增量数据(如恢复到某天某时的状态)?
- “全量备份+bin log”实现时间点恢复:
bin log是“追加写入”的持久化日志,按时间顺序记录所有变更,且不会被自动删除(可配置过期时间)。- 恢复时,先通过全量备份将数据库恢复到某个基准点,再通过
mysqlbinlog工具解析备份后的bin log,重放从基准点到目标时间点的所有变更,实现精准恢复。
bin log中记录的是主库的更新操作,既然如此,那redo log不也能记吗,为啥主从同步不用redo log呢?
主从同步的核心需求是:让从库精确复现主库的所有修改操作,且这个过程需要跨实例、跨存储引擎(甚至跨版本)兼容。
redo log中记录的操作,是与具体的物理存储引擎相关的,比如把物理地址为0x…的数据删了,在物理地址为0x…的存储单元后面插入一条数据,这玩意你同步到其他主机,物理地址就直接失效了,没法同步
- redo log 是基于主库的数据页物理地址的(比如说把物理地址为xxx的数据改成xxx),而从库的数据页分布与主库可能完全不同(即使数据相同,物理存储位置也可能不同),直接重放会导致数据错乱。而binlog 记录的是“修改逻辑”(如“把某行的某字段改成什么值”),从库只需执行相同的逻辑即可,无需关心数据的物理存储位置。
- 如果用 redo log 同步,从库必须使用与主库相同的存储引擎(如都是 InnoDB),否则无法解析物理日志。而 binlog 的逻辑操作与存储引擎无关,从库可以是任何支持 MySQL 协议的数据库(甚至非 MySQL 数据库,如某些数据仓库)。
- redo log 循环覆盖的特性,无法支持从库的断点续传(必须全量同步,效率极低)。bin log 追加写入且可标记位置,支持增量同步和断点续传,适合主从同步的高可用性需求。
总结:三个日志各自的作用
undo log:主要用于回滚redo log:主要用于保障数据持久化bin log:主要用于主从节点的数据同步
三者分工明确又协同工作(如事务提交时的“两阶段提交”需 redo log 和 bin log 配合),共同支撑数据库的事务一致性、高性能和可扩展性。
隔离性具体是如何实现的呢?
回滚操作的具体过程是如何实现的?(MVCC的介绍,什么是MVCC)
下面用一个具体的例子为你展现回滚的具体过程
多次UPDATE操作形成版本链
现在有一个事务1,对student表中记录进行修改(update):将name(张三)改成name(李四)。
-
在正式进行修改之前,我们需要把张三这条记录拷贝一份到 undo.log(mysql服务器内存中的一块缓冲区)中

-
然后我们修改表中张三这条记录,将name从张三改成李四,同时将历史记录在undo.log中的备份地址写入stu表这条记录的DB_ROLL_PTR中,将事务id写入DB_RTX_ID中

-
现在又来了一个事务2,目的是将这条记录中的name从李四改成王五,这时候同样的,我们要把李四这条记录也备份到undo.log中

- 然后 同样的,将这条记录中的name从李四改成王五,同时将这条记录在undo.log中的备份地址写入表中这条记录的DB_ROLL_PTR中,将事务id=11写入DB_RTX_ID中

像这样,每条历史记录中的DB_ROLL_PTR都存放着上一条历史记录在undo.log中的存放地址,这就形成了一条版本链(上面一个个的历史版本,我们就称之为一个个快照)
到此为止,我们的回滚操作就很好理解了,你想回滚到哪一步,其实就是用undo.log中对应的某条历史记录覆盖表中的记录
像这样通过版本链,对一条记录的多个版本进行控制的操作,我们就叫做MVCC
什么是MVCC?
前面已经说了,在三种常见的数据库并发场景中,我们重点讨论的是读写操作的并发问题。MVCC就是一种用来解决读写操作并发问题的无锁并发控制机制,全称是多版本并发控制
其实这个过程很像我们在OS中学到的写时拷贝(什么是写时拷贝?)
当有多个进程或对象引用同一份数据时,它们共享这份数据,而非各自拥有独立拷贝。只有当某个进程试图修改数据时,系统才会为其创建一份数据副本,让修改在副本上进行,其他进程仍使用原数据。
听你刚刚的介绍,我每做一次修改(update操作),这次修改前的历史版本就要被拷贝一份放到undo.log中,那我要是修改次数很多,记录的历史版本太多,把undo.log放满了怎么办?
不用担心,只要一个事务成功提交,这条事务执行过程中产生的所有历史版本都会从undo.log中清除,这样空间就腾出来了
因为undo.log中的快照是为了方便回滚,只要一个事务成功提交,那之后就不能回滚了,就用不到对应的历史版本了,所以这些历史记录就会被删了
一个事务中涉及到了对stu表中张三这条记录的修改操作,请问在这个事务提交之后,undo.log中还保存有张三这条记录的历史版本吗?
可能有,也可能没有
为什么可能有?因为当这个事务提交之后,如果还有正在执行的其他事务,涉及到对张三这条记录的访问操作,也就是说张三这条记录的历史记录还需要被用到,那么undo.log就不会立即清空它们(比如在RR的隔离级别下,很多事务用的都是张三这个表项在他们创建那一时刻的历史版本)
为什么可能没有?如果当这个事务提交之后,正在执行的其他事务,均不涉及到对张三这条记录的访问操作,也就是说张三这条记录的历史记录已经用不到了,那么undo.log就会把张三这条记录的历史记录全部清空
上面介绍了多次update操作会形成版本链,需要对多版本进行管理和维护,请问delete操作呢?删除一条记录会不会为这条记录形成一个新版本,插入到版本链中?insert操作呢?select操作呢?
delete操作
对一条记录进行delete操作,依然会为该条记录形成一个新版本,插入到版本链中。看下面的例子
实例:InnoDB中删除操作的版本链变化
假设某张表有一条记录,初始版本链如下(隐藏列:DB_TRX_ID(事务ID)、DB_ROLL_PTR(版本链指针)、DELETE_BIT(删除标记,部分版本隐含)):
| 数据内容 | DB_TRX_ID(事务ID) | DB_ROLL_PTR(指向) | DELETE_BIT(删除标记) |
|---|---|---|---|
| name: Alice | 100(插入事务) | NULL(无历史版本) | 0(未删除) |
当事务200执行DELETE操作后,版本链变化为:
- 不修改原版本(事务100的版本),而是生成一个新的“删除标记版本”,作为版本链头部;
- 新版本的
DB_TRX_ID=200(删除这条记录的事务ID为200),DB_ROLL_PTR指向原版本(事务100的版本),DELETE_BIT=1(已删除)。
最终版本链:
| 数据内容 | DB_TRX_ID | DB_ROLL_PTR(指向) | DELETE_BIT |
|---|---|---|---|
| (复用原数据,仅标记删除) | 200 | 事务100的版本 | 1 |
| name: Alice | 100 | NULL | 0 |
此时:
- 若有新事务读取该记录,会根据隔离级别判断:若事务200已提交,则读取到“删除标记版本”,判定记录已删除,返回空;若事务200未提交,则跳过“删除标记版本”,读取到事务100的版本(即“读已提交”/“可重复读”的体现)。
- 若事务200需要回滚,只需删除“删除标记版本”,版本链恢复为初始状态即可。
insert操作
在支持 MVCC 的数据库(以 MySQL InnoDB 为典型)中,INSERT 操作是版本链的“起点”——它会生成数据行的初始版本,这个版本会作为版本链的第一个节点;后续若该数据行被 UPDATE 或 DELETE,新的版本会通过 DB_ROLL_PTR(回滚指针)与初始版本串联,逐步形成完整的版本链。
下面举个粒子,假设我们有一张简单的用户表 user,结构如下:
CREATE TABLE user (
id INT PRIMARY KEY, -- 主键(避免InnoDB自动生成DB_ROW_ID)
name VARCHAR(50) NOT NULL,
age INT
) ENGINE=InnoDB; -- 必须是InnoDB(支持MVCC)
我们将模拟三个连续操作,观察版本链的变化:
- 事务 T1(ID=100):执行
INSERT插入一条记录,作为版本链的起点; - 事务 T2(ID=200):执行
UPDATE修改该记录,生成第二个版本; - 事务 T3(ID=300):执行
UPDATE再次修改该记录,生成第三个版本。
(注:事务 ID 是 InnoDB 内部生成的单调递增整数,用于标识事务归属;版本链的每个节点都包含 业务字段 + InnoDB 隐藏列,隐藏列是版本链串联的核心。)
步骤1:INSERT 操作——创建版本链的“初始节点”
当事务 T1(ID=100)执行 INSERT 时:
BEGIN; -- 启动事务 T1(ID=100)
INSERT INTO user (id, name, age) VALUES (1, '张三', 25);
COMMIT; -- 提交事务
此时版本链的状态:
版本链只有 1个初始节点(V1),这个节点是 INSERT 操作的直接产物,存储在 数据页(.ibd 文件) 中(最新版本优先存数据页,提升读取效率)。
每个版本节点包含两部分:业务字段 + 隐藏列,具体内容如下:
| 节点类型 | 业务字段(用户定义) | InnoDB 隐藏列(引擎维护) | 存储位置 |
|---|---|---|---|
| V1(初始版本) | id=1, name=‘张三’, age=25 | 1. DB_TRX_ID=100(生成该版本的事务ID,即T1) 2. DB_ROLL_PTR=NULL(无历史版本,版本链末尾) 3. DB_ROW_ID=省略(因表有主键id,无需自动生成) | 数据页(.ibd) |
关键说明:
INSERT操作只生成“初始版本”,因为它是数据的第一次创建,没有“历史版本”可关联,所以DB_ROLL_PTR=NULL(版本链的起点);- 事务提交后,
DB_TRX_ID=100会被标记为“已提交事务ID”,后续其他事务读取时,可通过该值判断 V1 是“已提交的有效版本”。
步骤2:第一次 UPDATE 操作——版本链扩展(新增 V2)
假设事务 T2(ID=200)执行 UPDATE 修改该记录:
BEGIN; -- 启动事务 T2(ID=200)
UPDATE user SET age=26 WHERE id=1; -- 将age从25改为26
COMMIT; -- 提交事务
此时版本链的变化:
UPDATE 操作不会修改原版本 V1,而是生成一个 新的版本 V2,并通过 DB_ROLL_PTR 将 V2 与 V1 串联,形成“V2 → V1”的版本链。具体细节如下:
-
新版本 V2 的内容:
- 业务字段:
id=1, name='张三', age=26(更新后的新值); - 隐藏列:
DB_TRX_ID=200(生成 V2 的事务ID,即 T2);DB_ROLL_PTR=指向 V1 的地址(将 V2 链接到历史版本 V1);DB_ROW_ID=省略;
- 存储位置:数据页(V2 成为最新版本,优先存数据页)。
- 业务字段:
-
历史版本 V1 的变化:
- 业务字段和隐藏列内容不变(仍为
age=25、DB_TRX_ID=100); - 存储位置:从数据页迁移到 Undo Log(历史版本存 Undo Log,节省数据页空间,仅保留最新版本)。
- 业务字段和隐藏列内容不变(仍为
此时完整的版本链:
V2(最新版本,存数据页) → V1(历史版本,存 Undo Log)
| 节点 | 业务字段 | 隐藏列(DB_TRX_ID / DB_ROLL_PTR) | 存储位置 |
|---|---|---|---|
| V2 | id=1, name=‘张三’, age=26 | 200 / 指向 V1 | 数据页 |
| V1 | id=1, name=‘张三’, age=25 | 100 / NULL | Undo Log |
步骤3:第二次 UPDATE 操作——版本链继续扩展(新增 V3)
再假设事务 T3(ID=300)执行 UPDATE 再次修改该记录:
BEGIN; -- 启动事务 T3(ID=300)
UPDATE user SET name='张三丰' WHERE id=1; -- 将name从'张三'改为'张三丰'
COMMIT; -- 提交事务
此时版本链的最终状态:
UPDATE 操作生成 新的版本 V3,通过 DB_ROLL_PTR 链接到 V2,版本链扩展为“V3 → V2 → V1”。具体细节:
-
新版本 V3 的内容:
- 业务字段:
id=1, name='张三丰', age=26; - 隐藏列:
DB_TRX_ID=300、DB_ROLL_PTR=指向 V2 的地址; - 存储位置:数据页(最新版本)。
- 业务字段:
-
历史版本的迁移:
- V2 从数据页迁移到 Undo Log(成为历史版本);
- V1 仍存于 Undo Log(保持不变)。
最终版本链结构:
V3(最新版本,数据页) → V2(历史版本,Undo Log) → V1(初始版本,Undo Log)
| 节点 | 业务字段 | 隐藏列(DB_TRX_ID / DB_ROLL_PTR) | 存储位置 |
|---|---|---|---|
| V3 | id=1, name=‘张三丰’, age=26 | 300 / 指向 V2 | 数据页 |
| V2 | id=1, name=‘张三’, age=26 | 200 / 指向 V1 | Undo Log |
| V1 | id=1, name=‘张三’, age=25 | 100 / NULL | Undo Log |
INSERT 形成版本链的特点
- 版本链的“起点”:
INSERT是版本链的第一个节点(初始版本 V1),后续所有UPDATE/DELETE生成的版本,都以 V1 为基础向后串联; - 无历史版本的特性:
INSERT生成的初始版本,DB_ROLL_PTR=NULL(没有历史版本可关联),是版本链的“根节点”; - 存储位置的初始分配:
INSERT的初始版本默认存于数据页(最新版本优先),后续被UPDATE后才会迁移到 Undo Log; - 事务归属的标记:初始版本的
DB_TRX_ID记录了执行INSERT的事务ID,是后续其他事务判断该版本“是否可见”的关键依据(如未提交的INSERT版本,其他事务无法看到)。
select操作
MVCC 中的版本本质是数据行的“状态快照”,版本链则是这些快照按时间顺序串联形成的链表(通过数据行的隐藏列 DB_ROLL_PTR 关联)。而版本的生成,仅发生在数据被修改(INSERT、UPDATE、DELETE)时——因为只有“修改”才会导致数据状态变化,需要保留历史状态以支持并发读的一致性。SELECT 作为“只读操作”,不改变数据状态,因此不涉及版本创建。
你隔离性不是也分好几个隔离等级吗?MVCC在哪里体现出了对隔离等级的划分呢?
事务在不同隔离级别下,被允许访问的快照是不同的。比如RR的隔离级别下,仅允许事务访问其创建时的历史版本,不允许事务访问数据库在其创建后的历史版本

当前读与快照读
当前读与快照读的概念
如果一个select语句读取的是某条记录的最新版本,我们就称这次读操作为当前读
如果一个select语句读取的是某条记录的历史版本,我们就称这次读操作为快照读
我们平时进行的select操作,属于当前读还是快照读呢?
这个问题不好回答的,它不仅与你使用的具体select指令有关,也与你指令所属的事务隔离级别有关
从隔离级别的角度回答
先说隔离级别吧,假如说你的隔离级别是读未提交,那select操作读的肯定就是最新的数据,属于当前读,如果你的隔离级别是读提交,那select操作读的就是已提交的最新数据(这个数据是已提交的最新,并不一定是真的最新),如果你的隔离级别是读未提交,那select操作读的就是事务创建时的历史数据,如果是串行化,那select操作读的也是最新版本的数据
这里肯定有人有问题,你RR级别下,select语句读的都是事务开始时候的历史版本,怎么串行化进一步限制之后,反而能读到最新的数据了呢?
关键在于 “限制的维度不同”:
- RR的“限制”是对“可见性”的限制(通过版本链过滤掉事务启动后的新修改),目的是让事务内看到的视图不变,但允许其他事务并发修改数据(只是当前事务看不到)。
- 串行化的“限制”是对“并发操作”的限制(通过锁禁止其他事务修改),目的是让数据在当前事务执行期间保持不变,因此可以直接读取最新版本(反正没人能改)。
简单说:
- RR通过“我看不见别人改的”实现隔离;
- 串行化通过“别人根本改不了”实现隔离。
后者的限制更彻底(禁止并发),因此不需要依赖版本链,直接读最新版本即可保证一致性。
隔离级别并非“限制越严格,就读取越旧的数据”,而是根据“解决并发问题的目标”选择不同的技术方案:
- RR为了“事务内重复读一致”,选择读“历史版本”(允许并发修改,但屏蔽新变化);
- 串行化为了“完全消除并发冲突”,选择读“最新版本”(禁止并发修改,因此最新版本在事务内不会变)。
两者的“限制”方向不同,导致读取的数据版本有差异,但其核心都是为了在“隔离性”和“并发性能”之间找到平衡(只是平衡点不同)。
从select指令格式的角度回答
如果输入正常的select指令,我们就默认是快照读(串行化的隔离级别下,select指令不是当前读,而是快照读)
如果输入select * from XXX lock in share mode或者是select * from XXX for update,这时的select就是当前读
还记得我们前面谈过MYSQL中的锁吗?
select * from XXX lock in share mode就是在读操作的同时,给对应的表项上了一把S锁(共享锁)
select * from XXX for update就是在读操作的同时,给对应的表项上了一把X锁(排他锁)
我们在进行update、DELETE、insert操作时,也都需要给对应的表项上一把X锁(排他锁)
表项上锁之后,其他想要修改该表项的事务都会在操作时陷入阻塞。这会极大地限制数据库的并发性能,牺牲并发性能换来的好处就是,此时的读操作就是当前读
多个事务能否同时对同一个表项进行当前读操作?
多个事务不能同时对同一个表项进行当前读操作!因为事务对某个表项进行当前读操作时,会给这个表项上一把X锁(排它锁),上锁之后其他事务再对当前表项进行当前读操作,肯定会阻塞。
那如果一个事务对某个表项进行当前读操作,给表项上锁之后,其他事务对当前表项进行普通的select操作,会不会阻塞呢?
答案是取决于上锁后执行select操作的事务的隔离级别
- 如果其他事务在 读未提交(RU)、读已提交(RC)、可重复读(RR) 隔离级别下执行普通的select操作,则不会被阻塞,因为它们使用 “快照读”(读取版本链中的历史版本,不加锁)
- 如果是其他事务在串行化的隔离级别下执行select操作,会被阻塞,因为串行化下普通的select操作要对当前表项上一把S锁(共享锁),而S锁与X锁是互斥的,如果想上S锁时发现该表项已经上了一把X锁,那该事务肯定会被阻塞
- 同理,如果其他事务在 读未提交(RU)、读已提交(RC)、可重复读(RR) 隔离级别下执行
select ... from XXX lock in share mode操作,则会被阻塞,因为S锁与X锁互斥 - 如果其他事务在任意隔离级别下执行
select * from XXX for update,那肯定会被阻塞,因为此时表项已经被上了一把X锁,你其他事务还想上,就得先阻塞等待到锁释放为止
多个事务能否同时对同一个表项进行快照读操作?
没问题,因为快照读不上锁。因此允许多个事务同时对同一个表项进行快照读操作,这就提高了效率
隔离性要解决什么问题?
到此为止,我们再来思考一下,隔离性和隔离级别要解决什么问题?
事务都是原子的。所以,无论如何,事务总有先有后。但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但不管怎么启动多个事务,总是有先有后的。那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,我们就需要让不同的事务看到它该看到的内容,不让它看到它不该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题
那什么是事务该看到的内容,什么是事务不该看到的内容呢?
这个不同隔离级别就不一样了
比如读未提交(RU)的隔离级别下,事务什么都看得到
RC的隔离级别下,事务不该看到其他没有提交的事务对数据库做的修改
RR的隔离级别下,事务不该看到从他创建起往后任何事物对数据库做的修改
既然如此,那我们如何保证让不同的事务看到不同的历史版本。从而实现事务的隔离性呢?请看read view介绍
read view介绍
什么是read view?
Read View 在 MySQL 源码中,就是一个结构体 ,一个 ReadView 结构体与 一个事务 强绑定。每当一个事务在执行过程中通过select进行快照读时,都需要创建一个类型为Read View 的对象,用来记录 “当前事务判断数据版本可见性的核心依据”
如何理解事务ID?事务ID是如何产生的?事务ID的大小有什么含义?
当每个事务开启时,都会被分配一个ID, 这个ID是按时间顺序递增的,所以越新的事务,ID值越大,越old的事务,ID越小
事务的readview结构体是在什么时候创建形成的?是事务开始的时候吗?
事务的ReadView(可见性视图)的创建时机,取决于事务的隔离级别,并非所有事务都是在“事务开始时”创建的。简单来说,只有RR级别在事务首次快照读时创建ReadView,且后续不变;RC级别则每次快照读都新建,并非在事务开始时固定创建。剩余两个
具体规则如下:
-
可重复读(RR)隔离级别
ReadView在事务第一次执行快照读操作时创建,且整个事务生命周期内只创建一次。 -
读已提交(RC)隔离级别
ReadView在事务每次执行快照读操作时都会重新创建。 -
读未提交(RU)和串行化(Serializable)隔离级别
- 读未提交(RU):不使用
ReadView,直接读取数据的最新版本(可能看到未提交的修改,即脏读)。 - 串行化(Serializable):不依赖
ReadView,而是通过加锁(普通SELECT会隐式加S锁)来保证串行执行,避免并发问题。
ReadView的创建时机由隔离级别决定:
- RR级别:第一次快照读时创建,全程复用(保证可重复读)。
- RC级别:每次快照读时重新创建(保证读已提交)。
- RU和Serializable级别:不使用
ReadView(RU直接读最新,Serializable靠加锁)。
因此,只有RR级别在事务首次快照读时创建ReadView,且后续不变;RC级别则每次快照读都新建,并非在事务开始时固定创建。
readview源码
现在我们来看一下readview这个类在Mysql中的源码

其实主要就是理解下面这四个变量
-
m_ids:
一张列表,用来维护这个Read View生成时系统正活跃(正在运行)的事务ID
所谓的活跃事务,指的就是正在进行中尚未提交的事务 -
up_limit_id:
记录m_ids列表中事务ID最小的ID -
low_limit_id:
ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1 -
creator_trx_id:
创建该ReadView的事务ID
有人看了之后可能会说,你up_limit_id代表的不应该是列表中最大的ID吗, low_limit_id代表的不应该是最小的ID吗,是不是弄反了呀?其实没弄反,就是这样的
如果一条记录的DB_TRX_ID<creator_trx_id,并且这个DB_TRX_ID不在m_ids中,那能否得出DB_TRX_ID<m_up_limit_id呢?
不能!我们举个例子,假如说当前事务的readview中,m_ids=[4,6,10,15,20],creator_trx_id=20,对于ID=6的这个事务,它的ID<creator_trx_id,并且不在m_ids中,很明显这个事务已经完成了,但是6>4,此时DB_TRX_ID > m_up_limit_id
有了readview之后,所有的事务就可以根据readview分成三类,请问是哪三类?
-
快照时已经提交的事务
事务ID<up_limit_id的事务 -
正在执行尚未提交的事务
m_ids列表中的事务 -
快照时还没开始的事务 (快照后新来的事务)
事务ID>=low_limit_id的事务

当前事务想对一条记录进行查找,我怎么知道到底应不应该让这个事务看到这条记录的最新版本呢?
还记得我们前面说的mysql表格中的3个记录隐藏列字段吗?mysql的表格中有3个记录隐藏列字段 ,分别叫做DB_TRX_ID ,DB_ROLL_PTR ,DB_ROW_ID
其实就是将该记录的DB_TRX_ID字段与readview中那几个参数进行比较,确认最后一次修改这条记录的事务与当前查看这条记录的事务的先后关系
if 该记录的DB_TRX_ID字段值 < readview中的m_creator_trx_id
说明这条记录最后一次修改的事务在当前事务之前
if 该记录的DB_TRX_ID字段值< readview中的m_up_limit_id
说明这条记录最后一次修改的事务在当前事务之前,并且已经完成提交了
if 该记录的DB_TRX_ID字段值>= low_limit_id
说明这条记录最后一次修改的事务创建时间在当前事务之后
然后再根据隔离级别,就能够知道这条记录的最新版本能不能给这个事务看了
对于隔离级别,我们先说RU和串行化这两种特殊情况
如果事务的隔离级别是读未提交,那当前事务能查看任意记录的最新版本(包括没有提交的版本)
如果事务的隔离级别是串行化,那当前事务能查看任意记录已提交的最新版本
然后我们再具体展开说说RC和RR,这两种隔离级别的判断逻辑有些差别,也有些共性,共性如下:
- 如果一条记录的DB_TRX_ID>=m_low_limit_id,那这条记录的最新版本肯定不能给当前事务看
m_low_limit_id是m_ids中最大的活跃事务ID,通常情况下和m_creator_trx_id相等 - 如果一条记录的DB_TRX_ID<creator_trx_id,并且这个DB_TRX_ID不在m_ids中,说明最后一次修改这条记录的事务在拍快照时已经提交结束了,那这条记录就可以直接给当前事务看
然后再说这俩判断逻辑的差别
RC隔离级别下,如果记录的DB_TRX_ID在当前事务readview的m_ids集合中,那该记录是不能给
看到这里可能有人会问,为啥当一条记录的DB_TRX_ID>=m_low_limit_id时,不管隔离级别如何,这条记录的最新版本都绝对不能给当前事务看呢?如果你的事务隔离级别设置为读提交,那不就允许当前事务查看已提交的更新记录了吗?
当一条记录的DB_TRX_ID >= m_low_limit_id时,说明最新修改该记录的事务一定是在当前ReadView生成之后才启动的事务
而RC级别下事务只能感知到自身创建时还没提交的事务对数据库的更新操作,无法感知“自身启动后、当前查询之后才启动的事务”的操作(即“未来事务”的修改)。因此即使是RC级别,这类“未来事务”的更新(无论是否已提交)都不会被当前事务的ReadView感知到,自然不可见。
然后我们再说说RR和RC隔离级别下判断逻辑的区别
- RC(读已提交):每次读取数据时,都会重新生成一个新的快照(即重新确定 m_ids 和 m_low_limit_id)。对于 DB_TRX_ID 在当前快照的 m_ids 范围内(即活跃事务修改的记录),RC 会实时检查该事务是否已提交:
若修改记录的事务已提交,则当前事务可以读取这条记录;
若未提交,则当前事务看不到这条记录(会读取其历史版本)。
因此,RC 能看到其他事务提交后的最新修改,可能出现 “不可重复读”。 - RR(可重复读):整个事务过程中只在首次读取时生成一个快照(m_ids 和 m_low_limit_id 固定不变)。对于 DB_TRX_ID 在初始快照的 m_ids 范围内(即活跃事务修改的记录),RR 不会实时检查该事务是否后续提交,而是直接认为这条记录 “不可见”,始终读取事务开始时的历史版本。
因此,RR 能保证事务内多次读取结果一致,避免 “不可重复读”,但可能看不到其他事务提交后的新修改。
不同隔离级别下ReadView的生成规则
不同隔离级别下ReadView的生成规则是不同的,由于RU和串行化不咋用ReadView,因此我们主要讨论RR和RC下ReadView的生成规则
- 可重复读(RR):一个事务在首次执行快照读时生成唯一的ReadView,后续所有快照读都复用这个ReadView,因此能保证“重复读”到相同的数据。
- 读提交(RC):每次执行快照读时都会重新生成一个新的ReadView。这意味着每次查询都会基于“当前时刻”的活跃事务状态来判断记录可见性,从而能看到“查询瞬间已提交的事务更新”。
注意,就是因为RR和RC在Readview的生成规则上做了区别,因此后面的判断逻辑用的就是同一份代码了
提问:RR和RC在快照读时会生成Readview,那进行当前读时会生成Readview吗?
不会!因为不需要。MVCC主要就是用来方便快照读的,当前读操作的数据一致性由锁来保障,不需要MVCC机制的Readview来保障
mysql中如何判断某个事务 ID(id)对应的修改是否对当前事务视图可见?
原理
我们可以看一下mysql中判断某个事务 ID(id)对应的修改是否对当前事务视图可见的源码

这段代码是 InnoDB 存储引擎中用于判断某个事务 ID(id)对应的修改是否对当前事务视图可见的函数 changes_visible。下面我们就来详细看看
-
bool changes_visible(trx_id_t id, const table_name_t& name) const:- 入参
id是要检查的事务 ID,name是表名(主要用于调试或校验上下文)。 - 返回值
bool:表示当前事务视图是否能“看到”id事务所做的修改。
- 入参
-
if(id < m_up_limit_id || id == m_creator_trx_id) return ture;m_up_limit_id:当前事务视图中,“最早的活跃事务 ID”(所有活跃事务的 ID 都大于等于它)。m_creator_trx_id:创建当前事务视图的事务 ID(即当前事务自己)。- 逻辑:如果
id小于“最早的活跃事务 ID”(说明id对应的事务在所有活跃事务开始前就已提交),或者id是当前事务自己的 ID(自己的修改对自己当然可见),则直接返回true(可见)。
-
校验事务 ID 合法性:
check_trx_id_sanity(id, name);- 内部逻辑:检查
id是否是合法的事务 ID(比如是否为保留值、是否超出范围等),若不合法会触发错误或断言。
- 内部逻辑:检查
-
if (id >= m_low_limit_id)m_low_limit_id:当前事务视图中,“最大的活跃事务 ID”(所有活跃事务的 ID 都小于等于它)。- 逻辑:如果
id大于等于“最大的活跃事务 ID”,说明id对应的事务是在当前视图生成之后才启动的(或还在活跃),因此它的修改对当前视图肯定不可见,返回false。
-
判断“活跃事务集合是否为空”:
else if (m_ids.empty())m_ids:当前事务视图生成时,所有活跃事务的 ID 集合。- 逻辑:如果活跃事务集合为空(说明视图生成时没有活跃事务),那么只要
id不满足“肯定不可见”(即id < m_low_limit_id),就说明id对应的事务在视图生成前已提交,因此返回true(可见)。
-
查询活跃事务集合:
return(!std::binary_search(p, p + m_ids.size(), id));- 逻辑:如果
m_ids不为空(视图生成时有活跃事务),则用二分查找,检查id是否在活跃事务的 ID 集合中: - 如果
id在m_ids中(说明视图生成时,id对应的事务还在活跃),则返回false(不可见); - 如果
id不在m_ids中(说明视图生成时,id对应的事务已提交),则返回true(可见)。
- 逻辑:如果
整个函数的核心是:通过事务 ID 与“视图生成时的活跃事务范围(m_up_limit_id、m_low_limit_id、m_ids)”的比较,判断某个事务的修改是否对当前事务视图可见,是 InnoDB 实现“多版本并发控制(MVCC)”的关键逻辑。
根据上面函数的判断,如果可以,那就直接给他看,如果最新版本不能给这个事务看,那我就通过版本链回溯,把该记录的某个历史版本给当前事务看
版本链回溯的方式:隐藏字段DB_ROLL_PTR中记录着上一个版本的记录在undo.log中的指针,这条记录的全部历史版本就通过这个指针串成了一个版本链。我只需要通过这个指针,从头开始遍历这个链表,一定能回溯到某个版本的DB_TRX_ID字段值符合当前隔离级别的要求,我就把这个版本的记录给当前事务看
下面我们再通过一个实例,加深我们对上述原理的理解
假设stu表中有一条记录如下图所示

然后有四个正在执行的事务操作(同一行表示同一时间),其中事务4的操作是修改name(张三) 变成name(李四) 。当事务2对某行数据执行了快照读时 ,数据库会为该行数据生成Read View 读视图,请问这个读视图中,m_ids、up_limit_id、low_limit_id、creator_trx_id 各是什么?

m_ids={1,3}
low_limit_id=5;
up_limit_id=1;
creator_trx_id=2;
当事务2对张三这一行记录进行快照读时,这行记录的版本链是什么?
首先我们要求出来该行记录的当前版本,也就是事务2对张三这一行数据进行快照读时,这条记录的name、DB_TRX_ID、DB_ROW_ID、DB_ROLL_PTR字段分别是多少
name='李四'
DB_TRX_ID=4
DB_ROW_ID=1(同一条记录,隐式主键值保持不变)
DB_ROLL_PTR=这条记录的历史版本在undo.log中的地址
然后给出版本链

事务2对张三这一行记录进行快照读时,看到的是该记录版本链中的哪一个版本?
就是最新版本,因为事务2快照时,事务4已经提交了
如何理解“某个事务中首次出现快照读的位置,决定该事务后续快照读结果的能力 ”这句话?
事务中快照读的结果非常依赖该事务首次出现快照读的地方
我们做个实验详细说明一下
实验环境搭建
设置RR模式下测试

创建表

插入数据

实验开始
同时开启两个事务,表中同一行对应的操作表示同一时间进行的操作
测试用例1


测试用例2


在用例1和用例2中,事务A的各操作执行时间都没有变化,唯一变化的仅仅是事务B的快照读操作发生的时间
在测试1中,事务B和事务A同时快照读,在测试2中,事务B在事务A,commit之后才快照读
实验结果显示:
测试1中事务B的快照读并没有读到事务A修改过后的结果,而测试2中事务B的快照读读到了
而两个测试中,无论快照的时机如何,事务B中当前读都能读到最新数据
为啥快照读的时间不同,会导致两次查询的结果不一样呢?
因为RR级别下的Readview就是在事务首次快照时形成的!第二次实验中事务2首次快照的时间晚,在事务1修改数据之后,因此快照时数据更新了!
当前读和快照读在RR级别下有什么区别 ?
当前读操作可以无视RR级别的封锁,读取到某条记录的最新版本
而快照读只能按照RR级别隔离性的限制,读取到快照时某条记录的历史版本
RR和RC在MVCC机制层面的本质区别是什么?
RR——可重复读
RC——读提交
RR和RC的本质区别是:
- 在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
- RR的readview,会随着快照(select操作)的更新而更新,而RC的readview不会更新,一直都是事务begin后的第一次快照形成的readview
并发读写的隔离性是如何实现的呢?
到这里,我们再来回答一下最初的问题,隔离性是如何实现的呢?
答:通过MVCC(多版本控制)+readview机制实现的
推荐阅读
关于这块,有很好的文章,推荐大家阅读
https://blog.csdn.net/SnailMann/article/details/94724197
https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html
https://blog.csdn.net/chenghan_yang/article/details/97630626
&spm=1001.2101.3001.5002&articleId=151125014&d=1&t=3&u=de36482c7993406a99c076858ca62904)
1232

被折叠的 条评论
为什么被折叠?



