深入理解数据库事务与MVCC机制

复制代码
感谢阅读!❤️
如果这篇文章对你有帮助,欢迎 **点赞** 👍 和 **关注** ⭐,获取更多实用技巧和干货内容!你的支持是我持续创作的动力!
**关注我,不错过每一篇精彩内容!**

目录

  • 一、Transaction事务
    • [📊 事务的四大特性](#📊 事务的四大特性)
    • [⏱️ 事务的生命周期](#⏱️ 事务的生命周期)
    • [💥 事务的并发的三个现象/问题](#💥 事务的并发的三个现象/问题)
    • [🛡️ 事务的隔离级别](#🛡️ 事务的隔离级别)
    • 🔧查看和设置事务隔离级别
    • [🧪 各个事务隔离级别测试](#🧪 各个事务隔离级别测试)
    • [👻 可重复读隔离级别下的幻读问题](#👻 可重复读隔离级别下的幻读问题)
  • 二、MVCC机制的核心原理
    • [❓ 什么是MVCC](#❓ 什么是MVCC)
    • [💡 MVCC作用](#💡 MVCC作用)
    • [🧠 MVCC原理](#🧠 MVCC原理)
    • [⚖️ MVCC在RC和RR下的不同表现](#⚖️ MVCC在RC和RR下的不同表现)
    • [👁️ 演示如何根据ReadView确定是哪个版本内容](#👁️ 演示如何根据ReadView确定是哪个版本内容)

一、Transaction事务

  • 事务是一个最小的工作单元,在数据库中,事务表示一件完整的事。
  • 一个业务的完成可能需要多条DML语句共同配合才能完成。例如:转账业务:需要执行两条DML语句,先更新张三账户的余额,再更新李四账户的余额,为了确保转账业务不出现问题,就必须保证这俩天DML语句要么同时成功,要么同时失败,怎么样保证同时成功或者同时失败呢?就需要使用事务机制
  • 也就是说用了事务机制之后,在同一个事务当中,多条DML语句会同时成功或者同时失败,不会出现一部分成功,一部分失败的现象。
  • 事务只针对DML语句有效:因为只有这三个语句是改变表中数据的。(insert、update、delete

📊 事务的四大特性

  • 原子性(Atomicity):事务中所有的操作要么全部成功完成,要么全部失败执行。

    例如:A像B转账1000元,必须同时完成"扣A 1000元 和 加 B 1000元",不能只做其中一步

  • 一致性(Consistency):事务执行前后,数据库中的数据应该保持一致的。

    例如:A和B的账户余额加起来一共20000元,不管它们之间进行多少次转账操作,总量20000元始终是不会变的,这就是事务的一致性。

  • 隔离性(Isolation):多个并发事务之间互不干扰,即使多个事务同时执行,每个事务都感觉像是独占数据库。

    例如:当多个用户并发访问数据库时,比如操作同一张表,数据库为每个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。事务就像每个教室一样,教室与教室之间有面墙,俩个教室中所发生的事情互不干扰。

  • 持久性(Durability):一旦事务被提交,那么对数据库的修改就是永久性的,即使数据库系统崩溃了也不会丢失提交事务的操作。通常通过写入日志(如Redo Logo)来实现的。


⏱️ 事务的生命周期

一个典型事务的生命周期包括以下阶段:

  • 开始事务(Begin/Start transaction
  • 执行一系列数据库操作(DML:Insert/Update/Delete
  • 提交事务或回滚事务(Commit:永久性保存修改;Rollback:撤销所有的修改)

只要执行了Commit 或者 Rollback,事务都会结束。

Mysql默认的事务机制是:自动提交事务,即只要执行一条DML语句则提交一次。

sql 复制代码
-- 测试数据:之后都会使用该测试数据
DROP TABLE IF EXISTS CUSTOMER;

CREATE TABLE CUSTOMER(
    ID INT(10) PRIMARY KEY AUTO_INCREMENT,
    NAME VARCHAR(255) NOT NULL
);

INSERT INTO CUSTOMER(NAME) VALUES
('Tom'),
('Jack'),
('Lily'),
('Nina'),
('Joao');

-- 插入一条数据
INSERT INTO CUSTOMER(NAME) VALUES('Fabio');

-- 回滚数据,是无效的,因为Mysql默认自动提交事务
ROLLBACK;

💥 事务的并发的三个现象/问题

当多个事务并发执行时,可能会出现以下现象/问题

问题 描述 示例
脏读(Dirty Read) 一个事务读取到了另一个未提交事务的修改数据 T1 读取 x = 100,T2 修改 x 为 200,但未提交事务,T1 再次读取 x 的值,x 变为 200 了
不可重复读 (Non-Repeatable Read) 同一个事务多次读取同一数据时,结果不一致 T1 读取 x = 100,T2 修改 x 为 200,并提交事务,T1 再次读取 x 的值,x 变为 200 了
幻读(Phantom Read) 同一个查询条件多次执行,返回的结果集的记录数不一样 T1 查询年龄 < 30 岁的用户有 5 人,T2 插入一个 25 岁的用户,并提交事务,T1 再次查询有 6 个用户

🛡️ 事务的隔离级别

设置不同的事务隔离级别,可以解决上述的并发问题

  • 隔离级别由低到高:读未提交< 读已提交 < 可重复读 < 串行化
  • 不同的隔离级别会存在不同的现象,现象按照严重性从高到低:脏读 > 不可重复读 > 幻读
  • 出现脏读,必然有不可重复读和幻读现象;出现不可重复读,必然有幻读现象。
  • Mysql的默认的事务隔离级别:可重复读
  • Oracle的默认事务隔离级别:读已提交
  • PostgreSQL的默认事务隔离级别:读已提交
隔离级别 脏读 不可重复读 幻读
读未提交(READ UNCOMMITTED) ✅ 存在 ✅ 存在 ✅ 存在
读已提交(RADM COMMITTED) ❌ 不存在 ✅ 存在 ✅ 存在
可重复读(REPEATABLE READ) ❌ 不存在 ❌ 不存在 ✅ 存在
串行化(SERIALIZABLE) ❌ 不存在 ❌ 不存在 ❌ 不存在

🔧查看和设置事务隔离级别

查看事务隔离级别

  • 查看当前会话的事务隔离级别
sql 复制代码
SELECT @@TRANSACTION_ISOLATION;
  • 查看全局的事务隔离级别
sql 复制代码
SELECT @@GLOBAL.TRANSACTION_ISOLATION;

设置事务隔离级别

  • 设置当前会话的事务隔离级别
sql 复制代码
SET TRANSACTION ISOLATION LEVEL <事务级别名称>

-- 示例
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 设置全局的事务隔离级别
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL <事务级别名称>

-- 示例
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

🧪 各个事务隔离级别测试

读未提交(READ UNCOMMITTED)

sql 复制代码
-- 验证是否会出现脏读现象

-- 设置全局事务隔离级别:READ UNCOMMITTED
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 重新开启俩个会话窗口,检查事务隔离级别是否修改成功,一个代表事务A,一个代表事务B,俩个事务使用同一个数据库中的同一张表
SELECT @@TRANSACTION_ISOLATION;

-- 事务A:开启事务
START TRANSACTION;

-- 事务B:开启事务
START TRANSACTION;

-- 事务A:查询CUSTOMER表中数据
SELECT * FROM CUSTOMER;

-- 事务B:插入一条新记录,不提交
INSERT INTO CUSTOMER(NAME) VALUES('Filip');

-- 事务A:再次查询CUSTOMER表中数据,发现读取到未提交事务B的新增的数据(出现了脏读)
SELECT * FROM CUSTOMER;

读已提交(READ COMMITTED)

sql 复制代码
-- 验证是否会出现脏读现象

-- 设置全局事务隔离级别:READ COMMITTED
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 重新开启俩个会话窗口,查询事务隔离级别
SELECT @@TRANSACTION_ISOLATION;

-- 事务A:开启事务
START TRANSACTION;

-- 事务B:开启事务
START TRANSACTION;

-- 事务A:查询CUSTOMER表中数据
SELECT * FROM CUSTOMER;

-- 事务B:插入一条新数据,不提交
INSERT INTO CUSTOMER(NAME) VALUES('张三');

-- 事务A:再次查询CUSTOMER表中数据,发现与之前结果一样
SELECT * FROM CUSTOMER;

-- 事务B:提交事务
COMMIT;

-- 事务A:再次查询CUSTOMER表中数据,查询到了事务B提交的数据(没出现脏读)
SELECT * FROM CUSTOMER;

-- 验证是否出现不可重复读现象

-- 事务A:查询ID=2的记录信息
SELECT * FROM CUSTOMER WHERE ID = 2;

-- 事务B:更新ID=2的记录信息,并提交事务
UPDATE CUSTOMER SET NAME = 'Jack111' WHERE ID = 2;
COMMIT;

-- 事务A:再次查询ID=2的记录信息,发现NAME被修改为了Jack111(出现了不可重复读现象)
SELECT * FROM CUSTOMER WHERE ID = 2;

可重复读(REPEATABLE READ)

sql 复制代码
-- 验证是否会出现不可重复读现象

-- 设置全局事务隔离级别:REPEATABLE READ
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 重新开启俩个会话窗口,查询事务隔离级别
SELECT @@TRANSACTION_ISOLATION;

-- 事务A:开启事务
START TRANSACTION;

-- 事务B:开启事务
START TRANSACTION;

-- 事务A:查询ID=3的记录信息
SELECT * FROM CUSTOMER WHERE ID = 3;

-- 事务B:更新ID=3的记录信息,并提交事务
UPDATE CUSTOMER SET NAME = 'Lily111' WHERE ID = 3;
COMMIT;

-- 事务A:再次查询ID=3的记录信息,和原先查询结果一样(没有出现不可重复读现象)
SELECT * FROM CUSTOMER WHERE ID = 3;

-- 验证是否会出现幻读

-- 事务A: 提交事务
COMMIT;

-- 事务A:开启事务
START TRANSACTION;

-- 事务B:开启事务
START TRANSACTION;

-- 事务A:查询ID在2~5之间的数据(4条记录)
SELECT * FROM CUSTOMER WHERE ID BETWEEN 2 AND 5;

-- 事务B: 删除ID=3的记录并提交事务
DELETE FROM CUSTOMER WHERE ID = 3;
COMMIT;

-- 事务A:查询ID在2~5之间的数据(使用的是"快照读",仍然是4条记录,没有出现幻读现象)
SELECT * FROM CUSTOMER WHERE ID BETWEEN 2 AND 5;

-- 事务A:查询ID在2~5之间的数据(使用"当前读",记录是3条记录,出现了幻读现象)
SELECT * FROM CUSTOMER WHERE ID BETWEEN 2 AND 5 FOR UPDATE;

快照读当前读,请查看本文中"可重复读隔离级别下的幻读问题"的内容。


串行化(SERIALIZABLE)

sql 复制代码
-- 如果不存在的话,就插入ID=3的数据
INSERT INTO CUSTOMER VALUES(3,'Lily');

-- 设置全局事务隔离级别:SERIALIZABLE
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 重新开启俩个会话窗口,查询事务隔离级别
SELECT @@TRANSACTION_ISOLATION;

-- 事务A:开启事务
START TRANSACTION;

-- 事务B:开启事务
START TRANSACTION;

-- 事务A:查询ID在2~5之间的数据(4条记录)
SELECT * FROM CUSTOMER WHERE ID BETWEEN 2 AND 5;

-- 事务B: 删除ID=3的记录,此时事务B会阻塞等待事务A完成,这条语句无法执行完毕
DELETE FROM CUSTOMER WHERE ID = 3;

-- 事务A:结束事务A,事务B立刻执行完刚刚阻塞的语句;
COMMIT;

-- 事务B:提交事务
COMMIT;

-- 查询ID在2~5之间的数据(就剩3条记录)
SELECT * FROM CUSTOMER WHERE ID BETWEEN 2 AND 5;

👻 可重复读隔离级别下的幻读问题

MySQL默认的隔离级别是可重复读,在很大程度上避免了幻读问题,但并不能完全解决。

在下列情况下会出现幻读问题:

  • 同一个事务中,先使用了"快照读",再使用当前读
  • 同一个事务中,先使用了"快照读",再使用了"DML语句",再使用了"快照读"

总结:

  • 执行DML语句之前,会进行一次"当前读";
  • 同一个事务中, 使用了俩种不同方式读,可能导致了幻读现象。
  • 都使用快照读或者都使用当前读,可以避免幻读现象。但不能保证多次读取数据之间不执行DML语句。
  • 快照读(MVCC方式):

    普通的 Select 语句就是快照读快照读 读取的是某个一致性快照 中的数据,在整个事务的处理过程中,执行相同的一个Select语句,每次都是读取快照(快照指的是固定的某个时刻的数据,就像现实世界中的拍照语言,把某个时刻的内容保留下来)。快照读 是通过MVCC方式解决幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即便中途有其他事务插入一条数据,也是查询不出来这条数据的,所以就很好的避免了幻读问题。

    快照读是如何解决幻读问题

    底层由MVCC(多版本并发控制)实现,实现方式是在开始事务后,在执行第一个查询语句后,会创建一个Read View,后续查询语句利用这个Read View,通过这个Read View就可以在Undo Log版本链中找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入新纪录,时查询不出来这条数据的,所以很好的避免了幻读问题。

  • 当前读(锁的方式):

    Select ... for update等语句就是当前读当前读 总是读取最新Committed数据。当前读 是通过next-key lock(记录锁和间隙锁)方式解决了幻读。因为当执行select ... for update语句时,会加上next-key lock,如果有其他事务在next-key lock锁范围内插入或删除一条记录,那么这个插入就会被阻塞,无法成功插入,所以很好的避免了幻读问题。执行DML语句之前,MySQL都会进行一次"当前读"。

    下面列举的这些语法的是当前读:

    sql 复制代码
    SELECT ... LOCK IN SHARE MODE
    SELECT ... FOR UPDATE
    INSERT ...
    DELETE ...
    UPDATE ...

当前读是如何解决幻读问题

select ... for update原理是:对查询范围内的数据进行加锁,不允许其他事务对这个范围内的数据进行增删改。即select语句 范围内的数据是不允许并发的,只能排队执行,从而避免幻读问题。

select ... for update加的锁叫做:next-key Lock。也称为记录锁 + 间隙锁。记录锁用来保证在锁定的数据范围内不允许 DeleteUpdate操作;间隙锁用来保证锁定的数据范围内不允许Insert操作。

假如有这样的学生数据表:

ID NAME
1 学生1
2 学生2
4 学生4

SQL语句是这样写的:

sql 复制代码
SELECT * FROM STUDENT WHERE ID BETWEEN 2 AND 4 FOR UPDATE;

那么ID[2, 4]区间的所有记录行都被锁定,不能删除或修改ID = 2ID = 4的数据是通过记录锁来完成的,不能插入ID = 3的数据是通过间隙锁来完成的。


二、MVCC机制的核心原理

❓ 什么是MVCC

MVCC全称Multi-Version Concurrency Control,即多版本并发控制。

多版本:指MySQL维护着行数据的多个版本

并发控制:在多个事务同时操作某一行记录时,MySQL控制返回多个版本的行记录中的某个版本。

这里的多版本指的是数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在。

版本控制结构示意图

💡 MVCC作用

MVCC的目的主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁。


🧠 MVCC原理

三个核心点:

  • 隐藏字段
  • Undo Log 版本链
  • ReadView

隐藏字段和Undo Log版本链决定了返回的数据

ReadView + 版本链访问规则/可见性算法 决定了返回哪个版本的数据

隐藏字段

对与使用InnoDB存储引擎的数据库表,它的聚簇索引记录中都包含俩个隐藏列

  • trx_id:当一个事务对某条聚簇索引记录进行改动时,就会把改事务的事务id记录在trx_id隐藏列中
  • roll_pointer:回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到Undo Log中,然后回滚指针指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

InnoDB中每个事务都有一个唯一的事务id,叫transaction id,它是在事务开始时向InnoDB的事务系统中申请的,是按申请顺序严格递增的。

聚簇索引记录结构图

Undo Log版本链

如上图所示,针对id = 1001这条数据,都会将旧数据放到一条Undo Log中,记作改记录的一个旧版本,随着更新次数的增多,所有版本都会被roll_pointer指针连成一个链表,把这个链表成为版本链版本链页记录 + Undo Log组成。根据这个版本链就能找到这条数据的历史版本。

根据这个例子理解版本链是如何生成的:

事务ID 操作类型 操作说明
(T1) 1 Insert 插入初始记录:Id=1001,Name="张三",Age=21
(T2) 2 Update 更新Id=1001的记录:Name="李四",Age=22
(T3) 3 Update 更新Id=1001的记录:Name="李四111",Age=23
sql 复制代码
-- 事务1:插入初始记录
INSERT INTO STUDENT(ID,NAME,AGE) VALUES(1001,"张三",21);

-- 事务2:开启事务,并更新ID = 1001的数据,提交事务
START TRANSACTION;

-- 事务3:开启事务,并更新ID = 1001的数据,不提交事务
START TRANSACTION;

-- 事务2:更新ID = 1001的数据并提交事务
UPDATE STUDENT SET NAME="李四",AGE=22 WHERE ID = 1001;
COMMIT;

-- 事务3:更新ID = 1001的数据并提交事务
UPDATE STUDENT SET NAME="李四111",AGE=23 WHERE ID = 1001;
COMMIT;

版本链示意图

plaintext 复制代码
+--------------------------------------------------------------------------------------+
|  聚簇索引 - 当前记录行 (最新版本,存储在磁盘/内存页中)                                 |
|  +----------------+----------------+----------------+----------------+--------------+ |
|  |  主键ID        |  trx_id        |  roll_pointer  |  name          |  age         | |
|  |  1001          |  T3            |  指向Undo Log3 |  李四111       |  23          | |
|  +----------------+----------------+----------------+----------------+--------------+ |
+--------------------------------------------------------------------------------------+
                                  ↓ (roll_pointer 指向)
+--------------------------------------------------------------------------------------+
|  Undo Log3 (UPDATE_UNDO - T3事务更新前的版本)                                       |
|  +----------------+----------------+----------------+----------------+--------------+ |
|  |  trx_id        |  roll_pointer  |  name          |  age           |  其他字段     | |
|  |  T2            |  指向Undo Log2 |  李四         |  22              |  ...         | |
|  +----------------+----------------+----------------+----------------+--------------+ |
+--------------------------------------------------------------------------------------+
                                  ↓ (roll_pointer 指向)
+--------------------------------------------------------------------------------------+
|  Undo Log2 (UPDATE_UNDO - T2事务更新前的版本)                                        |
|  +----------------+----------------+----------------+----------------+--------------+ |
|  |  trx_id        |  roll_pointer  |  name          |  age           |  其他字段     | |
|  |  T1            |  指向Undo Log1 |  张三           |  21            |  ...         | |
|  +----------------+----------------+----------------+----------------+--------------+ |
+--------------------------------------------------------------------------------------+
                                  ↓ (roll_pointer 指向)
+--------------------------------------------------------------------------------------+
|  Undo Log1 (INSERT_UNDO - T1事务插入的初始版本,仅用于事务回滚,不参与MVCC读)          |
|  +----------------+----------------+----------------+----------------+--------------+ |
|  |  trx_id        |  roll_pointer  |  name          |  age           |  其他字段     | |
|  |  T1            |  NULL          |  张三           |  21            |  ...         | |
|  +----------------+----------------+----------------+----------------+--------------+ |
+--------------------------------------------------------------------------------------+
版本链示意图

ReadView

利用undo Log已经保留下了各个版本的数据,现在关键的问题是需要读取哪个版本的数据。这时候就需要用ReadView

ReadView 就是事务在使用 MVCC 机制进行快照读操作时产生的一致性视图。

ReadView包含 4 个关键属性,这些属性是判断版本可见性的核心依据:

字段名称 英文标识 核心含义
当前活跃事务 ID 集合 m_ids 生成 ReadView 时,当前数据库中所有未提交的活跃事务 ID的集合(不包含当前事务自身)
最小活跃事务 ID min_trx_id m_ids 集合中的最小值(当前最早的活跃事务 ID)
最大事务 ID + 1 max_trx_id 生成 ReadView 时,数据库中即将分配的下一个事务 ID(即当前已存在的最大事务 ID + 1)
当前事务 ID creator_trx_id 生成该 ReadView当前事务自身的 ID
ReadView结构示意图

ReadView版本链数据访问规则

trx_id是当前事务ID

规则序号 判断条件 结论(是否可访问版本) 补充说明
1 trx_id = creator_trx_id 可以访问该版本 成立,说明数据是当前这个事务更改的。(trx_id:代表当前记录版本的事务 ID;creator_trx_id:代表生成 ReadView 的当前事务 ID)
2 trx_id < min_trx_id 可以访问该版本 成立,说明数据已经提交了。(min_trx_idReadView 中记录的最小活跃事务 ID
3 trx_id > max_trx_id 不可以访问该版本 成立,说明该事务是在 ReadView 生成后才开启。(max_trx_idReadView 中记录的最大事务 ID+1
4 min_trx_id <= trx_id <= max_trx_id trx_id 不在 m_ids 中,可访问该版本 成立,说明数据已经提交。(m_idsReadView 生成时,数据库中所有未提交的活跃

⚖️ MVCC在RC和RR下的不同表现

  • Read Committed:每次快照读生成新的ReadView
  • Repeatable Read:只有第一次快照读生成ReadView,后面复用。

同事务内,Read Committed 隔离级别下,可能俩次快照读返回的是不同版本的记录;而Repeatable Read 隔离基本下则俩次快照读返回的是相同的版本记录。

这解释了为什么Read Committed 隔离级别下有不可重复读问题,而Repeatable Read 隔离基本是可重复读

👁️ 演示如何根据ReadView确定是哪个版本内容

用上述例子做演示并且是在可重复读隔离级别下做演示:

sql 复制代码
-- 事务1 (trx_id=1):插入初始记录
INSERT INTO STUDENT(ID,NAME,AGE) VALUES(1001,"张三",21);

-- 事务2(trx_id=2):开启事务,并更新ID = 1001的数据,提交事务
START TRANSACTION;

-- 事务3(trx_id=3):开启事务,并更新ID = 1001的数据,不提交事务
START TRANSACTION;

-- 事务4(trx_id=4):开启事务,并第一次查询ID=1001的数据(此时生成了ReadView)
START TRANSACTION;
SELECT * FROM STUDENT WHERE ID = 1001;

-- 事务2(trx_id=2):更新ID = 1001的数据并提交事务
UPDATE STUDENT SET NAME="李四",AGE=22 WHERE ID = 1001;
COMMIT;

-- 事务3(trx_id=3):更新ID = 1001的数据并提交事务
UPDATE STUDENT SET NAME="李四111",AGE=23 WHERE ID = 1001;
COMMIT;

现在已经生成了版本链 ,和ReadView

ReadView信息如下:

creator_trx_id m_ids min_trx_id max_trx_id
4 [2, 3] 2 5

当开启事务4时,查询ID=1001的数据应该显示哪个呢?

sql 复制代码
-- 事务4:再次查询ID=1004
SELECT * FROM STUDENT WHERE ID = 1001;

根据这个版本链示意图,一步一步分析:

  • 第一步:事务 4 再次执行查询时,从版本链的页记录开始,页记录的trx_id = 3 ,因为trx_id < creator_trx_id 并且trx_id是在m_ids中,所以页记录不可见。
  • 第二步:顺着roll_pointer进入到Undo Log中寻找到第一条记录(即row_id = 3),trx_id = 2 因为trx_id < creator_trx_id 并且trx_id是在m_ids中,所以当前版本不可见。
  • 第三步:顺着roll_pointer进入到Undo Log中寻找到第下一条记录(即row_id = 2),trx_id = 1 因为trx_id < creator_trx_id 并且trx_id < min_trx_id,所以当前版本可见,返回该版本数据。

所以查询到的结果还是:NAME = "张三", AGE = 21

相关推荐
2201_757830872 小时前
AOP核心概念
java·前端·数据库
JIngJaneIL2 小时前
基于java+ vue学生成绩管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端
爱码小白2 小时前
logging输出日志
数据库
老华带你飞2 小时前
智能菜谱推荐|基于java + vue智能菜谱推荐系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
曹牧2 小时前
Oracle:IN子句,参数化查询
数据库·oracle
篱笆院的狗3 小时前
Group by很慢,如何定位?如何优化?
java·数据库
李宥小哥3 小时前
SQLite01-入门
数据库
老邓计算机毕设3 小时前
SSM校园服装租赁系统864e2(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·校园服装租赁系统