MySQL事务知识复习

1、请讲解一下事务的隔离级别:

1.1、什么是事务?

事务就是把一组sql语句打包成一个整体,在这组sql语句中要么全部执行成功,要么全部失败。这组sql可以是一条也可以是多条。(事务是数据库管理系统执行过程中的一个逻辑单元,包含了一组数据库操作,要么全部成功执行,要么全部失败回滚,以保证数据的一致性、完整性和可靠性)

举个例子来说的话:比如转账。张三和李四都有1000元钱,现在张三向李四转100元那么张三剩下900元,李四剩下1100元钱。在这个例子中(1)张三和李四的钱的总和一定是2000元钱不能超过或比这个钱少。(2)不能出现张三钱减少而李四钱不增多的情况。(3)转账后的余额应当保存到存储介质中方便提取。(4)张三和李四的转账不能因为其他转账而收到干扰。

1.2、事务的ACID特性:

原子性、一致性、隔离性、持久性。

1.2.1、原子性:

一个事务中的所有操作,要么全部成功,要么全部失败,不会出现执行一半的情况,如果事务在执行过程中发生了错误,会回滚到事务开始前的状态,就像这个事务重来没有执行过一样。

sql 复制代码
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
-- 如果第二条语句失败,第一条也会回滚
COMMIT;

简短回答:原子性要求事务的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务中的操作不能只执行其中一部分。

1.2.2、一致性:

在事务开始之前和事务结束之后,数据库的完整性不会被破坏。这表明了写入的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服务器崩溃和如何恢复。

一致性确保事务从一个一致的状态转换到另一个一致的状态。

比如在银行转账事务中,无论发生什么,转账前后两个账户的总金额应保持不变。假如A账户(100 块)给 B 账户(10 块)转了10块钱,不管成功与否,A和B的总金额都是110块。

sql 复制代码
-- 假设 A 账户余额为 100,B 账户余额为 10

-- 转账前状态
SELECT balance FROM accounts WHERE user_id = 'A'; -- 100
SELECT balance FROM accounts WHERE user_id = 'B'; -- 10

-- 转账操作
START TRANSACTION;
UPDATE accounts SET balance = balance - 10 WHERE user_id = 'A';
UPDATE accounts SET balance = balance + 10 WHERE user_id = 'B';
COMMIT;

-- 转账后状态
SELECT balance FROM accounts WHERE user_id = 'A'; -- 90
SELECT balance FROM accounts WHERE user_id = 'B'; -- 20`
-- 总金额仍然是 110

简短回答:一致性确保数据的状态从一个一致状态转变为另一个一致状态。一致性与业务规则有关,比如银行转账,不论事务成功还是失败,转账双方的总金额应该是不变的。

1.2.3、隔离性:

数据库运行多个并发事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行导致数据不一致的情况。事务可以指定不同的隔离级别,以权衡在不同场景下数据库性能和安全。

隔离性意味着并发执行的事务是彼此隔离的,一个事务的执行不会被其他事务干扰。事务之间是井水不犯河水的。隔离性主要是为了解决事务并发执行时可能出现的脏读、不可重复读、幻读等问题。

比如说在读未提交的隔离级别下,会出现脏读现象:一个事务C读取了事务B尚未提交的修改数据。如果事务B最终回滚,事务C读取的数据就是无效的"脏数据"。

sql 复制代码
-- 会话 A
-- 创建模拟并发的测试表
DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    balance DECIMAL(10,2)
);

-- 插入测试数据
INSERT INTO accounts (name, balance) VALUES
('王二', 1000.00),
('张三', 2000.00),
('李四', 3000.00);

-- 会话B 中,设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;

-- 在会话 B 中更新数据但不提交
UPDATE accounts SET balance = balance - 500 WHERE name='王二';

-- 会话C 是读为提交级别,读取数据,得到 500
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM accounts WHERE name='王二';
-- 继续别的操作,基于 500

-- 会话 B 的事务回滚,导致会话 A 读到的数据其实是脏数据
ROLLBACK;

通过升级隔离级别为读已提交可以解决脏读的问题。

读已提交是指:只能读到别的事务"已经提交"的数据,读不到未提交的脏数据。一句话解释:你在事务里查询数据,只能看到别人已经提交完成的数据,看不到别人正在改、还没提交的中间数据。

sql 复制代码
-- 会话 B 修改为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 执行第一次查询 1000
SELECT * FROM accounts WHERE name='王二';

-- 会话 C 中,设置隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 在会话 C 中更新数据但不提交
START TRANSACTION;
UPDATE accounts SET balance = balance + 200 WHERE name='王二';

-- 会话 B 中再次读取数据,结果仍然为 1000
SELECT * FROM accounts WHERE name='王二';

-- 会话 C 中回滚事务
ROLLBACK;
-- 会话 B 中再次读取数据,结果仍然为 1000
SELECT * FROM accounts WHERE name='王二';

但会出现不可重复读的问题:事务B第一次读取某行数据值为X,期间事务C修改该数据为Y并提交,事务B再次读取时发现值变为Y,导致两次读取结果不一致。

sql 复制代码
-- 会话 B 修改为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 执行第一次查询 1000
START TRANSACTION;
SELECT * FROM accounts WHERE name='王二';

-- 会话 C 中,设置隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 在会话 C 中更新数据并提交
START TRANSACTION;
UPDATE accounts SET balance = balance + 200 WHERE name='王二';
-- 会话 C 提交事务
COMMIT;

-- 会话 B 中再次读取数据,结果仍然为 1200
SELECT * FROM accounts WHERE name='王二';

可以通过升级隔离级别为可重复读来解决不可重复读的问题。

可重复读是指:同一个事务内,多次读取同一行数据,结果完全一样,不受其他事务提交的影响。定义是:是数据库事务的隔离级别,保证在同一个事务内多次读取同一数据,结果保持一致,不受其他已提交事务的修改影响,能避免脏读、不可重复读,是 InnoDB 的默认隔离级别。用最通俗的话讲:你开启一个事务后:不管别人怎么改、怎么提交,你在这个事务里反复查同一条数据,查到的永远是你刚开启事务时的那个版本这就叫可重复读。

sql 复制代码
-- 会话 B 修改为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 开始事务并执行第一次查询 1000
START TRANSACTION;
SELECT * FROM accounts WHERE name='王二';

-- 会话 C 中,设置隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 在会话 C 中更新数据并提交
START TRANSACTION;
UPDATE accounts SET balance = balance + 200 WHERE name='王二';
-- 会话 C 提交事务
COMMIT;

-- 会话 B 中再次读取数据,结果仍然为 1000
SELECT * FROM accounts WHERE name='王二';

但可重复读级别下仍然会出现幻读的问题:事务B第一次查询获得2条数据,事务C 新增1条数据并提交后,事务B再次查询时仍然为2条数据,但可以更新新增的数据,再次查询时就发现有3条数据了。通俗的来说,幻读就是在同一个事务里:第一次查询:返回10条,别人插入/删除了数据并提交,你再查:变成11条或9条→像幻觉一样多/少了行,这就是幻读。

sql 复制代码
-- 会话 B 修改为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 执行第一次查询,查到 2 条记录
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 1000;

-- 会话 C 中,设置隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 在会话 C 中新增数据并提交
START TRANSACTION;
INSERT INTO accounts (name, balance) VALUES ('王五', 4000);
-- 会话 C 提交事务
COMMIT;

-- 会话 B 中再次读取数据,结果仍然为 2 条
SELECT * FROM accounts WHERE balance > 1000;
-- 会话 B 中尝试更新王五的余额为 5000,竟然成功了
UPDATE accounts SET balance = 5000 WHERE name='王五';
-- 会话 B 中再次读取数据,发现 3 条记录
SELECT * FROM accounts WHERE balance > 1000;

可以通过升级隔离级别为串行化来解决幻读的问题。

隔离级别 是否会脏读 是否会不可重复读 是否会幻读
Read Uncommitted(读未提交) ✅ 可能 ✅ 可能 ✅ 可能
Read Committed(读已提交) ❌ 不会 ✅ 可能 ✅ 可能
Repeatable Read(可重复读) ❌ 不会 ❌ 不会 ✅ 可能(但 InnoDB 已解决)
Serializable(可串行化) ❌ 不会 ❌ 不会 ❌ 不会

简短回答:多个并发事务之间需要相互隔离,即一个事务的执行不能被其他事务干扰。

1.2.4、追问:企业一般不适用Serializable(可串行化),因为使用Serializable会导致性能较差,哪InnoDB是怎么样去解决这个问题的呢?

InnoDB(可重复读(RR))级别下,通过Next-Key Lock(临键锁) 彻底解决了幻读。临键锁会锁住查询范围的所有数据与间隙,阻止其他事务插入新数据,从而避免幻读。

1.2.5、持久性:

事务处理结束之后,对数据的修改将永久的保存到存储介质中,即便是系统故障了也不会消失。

持久性确保事务一旦提交,它对数据所做的更改就是永久性的,即使系统发生崩溃,数据也能恢复到最近一次提交的状态。

MySQL的持久性是通过InnoDB引擎的redo log实现的。在事务提交时,InnoDB会先将修改操作写入redo log,并刷盘持久化。崩溃后,InnoDB会通过redo log恢复数据,从而保证事务提交成功的数据不会丢失。

简短回答:一旦事务提交,则其所做的修改将永久保存到MySQL中。即使发生系统崩溃,修改的数据也不会丢失。

1.3、为什么使用事务?

事务具备的ACID特性,是我们使用事务的原因,在我们日常的业务场景中有大量的需求要用事务来保证。支持事务的数据库能够简化我们的编程模型, 不需要我们去考虑各种各样的潜在错误和并发问题,在使用事务过程中,要么提交,要么回滚,不用去考虑网络异常,服务器宕机等其他因素,因此 我们经常接触的事务本质上是数据库对ACID模型的⼀个实现,是为应用层服务的。

2、ACID靠什么来保证的呢?

一句话总结:ACID中的原子性主要通过Undo Log来实现,持久性通过Redo Log来实现,隔离性由MVCC和锁机制来实现,一致性则由其他三大特性共同保证。

2.1、详细说说如何保证原子性?

事务对数据进行修改前,会记录一份快照到Undo Log,如果事务中有任何一步执行失败,系统会读取Undo Log将所有操作回滚,恢复到事务开始前的状态,从而保证事务要么全部成功,要么全部失败。

sql 复制代码
1)BEGIN;

2)UPDATE user SET balance = balance - 100 WHERE id = 1;
   => 写入 Undo Log:记录 id=1 的原始余额 500

3)UPDATE user SET balance = balance + 100 WHERE id = 2;
   => 写入 Undo Log:记录 id=2 的原始余额 300

4)COMMIT;
   => 清空 Undo Log,事务成功

❗如果失败:
   => 执行 ROLLBACK:根据 Undo Log 把数据还原!

2.2、详细说说如何保证持久性?

MySQL 的持久性主要由预写Redo Log、双写机制、两阶段提交以及Checkpoint刷盘机制共同保证。

当事务提交时,MySQL会先将事务的修改操作写入Redo Log,并强制刷盘,然后再将内存中的数据页刷入磁盘。这样即使系统崩溃,重启后也能通过Redo Log重放恢复数据。

在将数据页写入到磁盘时,如果发生崩溃,可能会导致数据页不完整。InnoDB的数据页大小为16KB,通常大于操作系统的 4KB页大小。

为了解决只写入部分的问题,MySQL采用了双写机制,脏盘刷页时,先将数据页写入到一个双写缓冲区中,2M的连续空间,然后再将其写入到磁盘的实际位置。

崩溃恢复时,如果发现数据页不完整,会从双写缓冲区中恢复副本,确保数据页的完整性。

在涉及主从复制时,MySQL通过两阶段提交保证Redo Log和Binlog的一致性:第一阶段,写入Redo Log并标记为prepare状态;第二阶段,写入Binlog再提交Redo Log为commit状态。

崩溃恢复时,如果发现Redo Log是 prepare但Binlog完整,则会提交事务;反之会回滚,避免主从不一致。

另外,由于Redo Log的容量有限,Checkpoint机制会定期将内存中的脏页刷到磁盘,这样能减少崩溃恢复时需要处理的Redo Log数量。

2.3、请详细说说如何保证隔离性?

隔离性主要通过锁机制和MVCC来实现。

比如说一个事务正在修改某条数据时,MySQL会通过临键锁来防止其他事务同时进行修改,避免数据冲突。

同时,临键锁可以防止幻读现象的发生。比如事务A查询id > 10的记录,那么临键锁不仅会锁住id=10的行,还会锁住10后面的"间隙",防止其他事务插入id=15的数据。

假如表中的主键有id: 5, 10, 15, 20, 25,那么InnoDB会对以下区间和记录加锁:

加锁对象 类型 锁定含义
(10, 15] 临键锁 锁住 id=15 和前间隙,防止插入11~14
(15, 20] 临键锁 锁住了 id=20 和前间隙
(20, 25] 临键锁 锁住了 id=25 和前间隙
(25, +∞) 间隙锁 锁住尾部防止插入30等

MVCC 主要用来优化读操作,通过保存数据的历史版本,让读操作不需要加锁就能直接读取快照,提高读的并发性能。

不同的隔离级别对应不同的实现策略,比如说在可重复读隔离级别下,事务第一次查询时会生成一个Read View,之后所有读操作都复用这个视图,保证多次读取的结果一致。

2.3、如何保证一致性?

MySQL的一致性并不是靠某一个机制单独保证的,而是原子性、隔离性和持久性协同作用的结果。

2.4、事务会不会自动提交?

是的,MySQL默认开启了事务自动提交模式。

每条单独的SQL语句都会被视为一个独立的事务处理单元;SQL语句执行成功后会自动执行 COMMIT;执行失败时会自动ROLLBACK。

可通过SELECT@@autocommit;查看当前会话的自动提交状态。

如果需要执行多条SQL语句,可以将它们放在一个事务中,使用START TRANSACTION开启事务,执行完所有SQL语句后手动提交。

sql 复制代码
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;

3、事务的隔离级别有那些?

隔离级别定义了一个事务可能受其他事务影响的程度,MySQL支持四种隔离级别,分别是:读未提交、读已提交、可重复读和串行化。

读未提交会出现脏读,读已提交会出现不可重复读,可重复读是InnoDB默认的隔离级别,可以避免脏读和不可重复读,但会出现幻读。不过通过MVCC和临键锁,能够防止大多数并发问题。

串行化最安全,但性能较差,通常不推荐使用。

3.1、详细说说读未提交

事务可以读取其他未提交事务修改的数据。也就是说,如果未提交的事务一旦回滚,读取到的数据就会变成了"脏数据",通常不会使用。

3.2、什么是读已提交?

读已提交避免了脏读,但可能会出现不可重复读,即同一事务内多次读取同一数据结果会不同,因为其他事务提交的修改,对当前事务是可见的。是 Oracle、SQL Server 等数据库的默认隔离级别。

3.3、​什么是可重复读?​

可重复读能确保同一事务内多次读取相同数据的结果一致,即使其他事务已提交修改。是 MySQL 默认的隔离级别,避免了"脏读"和"不可重复读",通过 MVCC 和临键锁也能在一定程度上避免幻读。

sql 复制代码
-- Session A:
START TRANSACTION;
SELECT balance FROM accounts WHERE id=1; --返回500

-- Session B:
UPDATE accounts SET balance = balance +100 WHERE id=1;
COMMIT;

-- Session A再次查询:
SELECT balance FROM accounts WHERE id=1; --仍返回500(可重复读)

-- Session A更新后查询:
UPDATE accounts SET balance = balance +50 WHERE id=1; --基于最新值550更新为600 
SELECT balance FROM accounts WHERE id=1; --返回600

3.4、什么是串行化?

串行化是最高的隔离级别,通过强制事务串行执行来解决"幻读"问题。但会导致大量的锁竞争问题,实际应用中很少用。

3.5、A 事务未提交,B 事务上查询到的是旧值还是新值?

如果B是普通的SELECT,也就是快照读,它读的是旧值,即事务A修改前的快照,并且不会阻塞;如果B是当前读,比如SELECT ... FOR UPDATE,它会被阻塞直到事务A提交或回滚。

sql 复制代码
-- 会话 A 中,更新王二的余额
START TRANSACTION;
UPDATE accounts SET balance = 8000 WHERE name = '王二';
-- 此时并没有 COMMIT

-- 会话 B 中查询王二的余额
SELECT * FROM accounts WHERE name = '王二';
-- 会话 B 会读取到 旧值 1000

-- 会话 C 中使用当前读查询王二的余额
SELECT * FROM accounts WHERE name = '王二' FOR UPDATE;
-- 会话 C 会被阻塞,直到会话 A 提交或回滚

3.6、怎么更改事务的隔离级别?

MySQL支持通过SET语句修改事务隔离级别,包括全局级别、当前会话,但一般不建议在生产环境中随意修改隔离级别。

测试环境下可以使用SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; 可以修改当前会话的隔离级别。

使用SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; 可以修改全局隔离级别,影响新的连接,但不会改变现有会话。

4、事务的隔离级别是如何实现的?

读未提交通过行锁共享锁确保一个事务在更新行数据但没有提交的情况下,其他事务不能更新该行数据,但不会阻止脏读,意味着事务2可以在事务1提交之前读取到事务1修改的数据。

读已提交会在更新数据前加行级排他锁,不允许其他事务写入或者读取未提交的数据,也就意味着事务2不能在事务1提交之前读取到事务1修改的数据,从而解决脏读的问题。

另外,读已提交会在每次读取数据前都生成一个新的ReadView,所以会出现不可重复读的问题。

可重复读只在第一次读操作时生成ReadView,后续读操作都会使用这个ReadView,从而避免不可重复读的问题。

另外,对于当前读操作,可重复读会通过临键锁来锁住当前行和前间隙,防止其他事务在这个范围内插入数据,从而避免幻读的问题。

串行化级别下,事务在读操作时,会先加表级共享锁;在写操作时,会先加表级排他锁。

直到事务结束后才释放锁,这样就能确保事务之间不会相互干扰。

5、请详细说说幻读呢?

幻读是指在同一个事务中,多次执行相同的范围查询,结果却不同。这种现象通常发生在其他事务在两次查询之间插入或删除了符合当前查询条件的数据。

比如说事务A在第一次查询某个条件范围的数据行后,事务B插入了一条新数据且符合条件范围,事务A再次查询时,发现多了一条数据。

我们来验证一下,先创建测试表,插入测试数据。

sql 复制代码
CREATE TABLE `user_info` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `name` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '姓名',
  `gender` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '性别',
  `email` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮箱',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

-- 插入测试数据
INSERT INTO `user_info` (`id`, `name`, `gender`, `email`) VALUES 
  (1, 'Curry', '男', 'curry@163.com'),
  (2, 'Wade', '男', 'wade@163.com'),
  (3, 'James', '男', 'james@163.com');

COMMIT;

然后我们在事务A中执行查询SELECT * FROM user_info WHERE id > 1;,在事务B中插入数据INSERT INTO user_info (name, gender, email) VALUES ('wanger', '女', 'wanger@163.com');,再在事务A中修改刚刚插入的数据 update user_info set gender='男' where id = 4;,最后在事务A中再次查询SELECT * FROM user_info WHERE id > 1;。

5.1、如何避免幻读?

MySQL 在可重复读隔离级别下,通过MVCC和临键锁可以在一定程度上避免幻读。比如说在查询时显示加锁,利用临键锁锁定查询范围,防止其他事务插入新的数据。

sql 复制代码
START TRANSACTION;
SELECT * FROM user_info WHERE id > 1 FOR UPDATE; -- 加临键锁
COMMIT;

其他事务在插入数据时,会被阻塞,直到当前事务提交或回滚。

5.1.1、对这个进行解释。

如果查询语句中包含显式加锁(如 FOR UPDATE),InnoDB z会使用当前读,直接读取最新的数据,并加锁。

在范围查询时,InnoDB不仅会对符合条件的记录加行锁,还会对相邻的索引间隙加间隙锁,从而形成临键锁。

临键锁可以防止其他事务在间隙中插入新数据,从而避免幻读。

比如说在执行查询的事务中,不要尝试去更新其他事务插入/删除的数据,利用快照读来避免幻读。

使用SELECT查询时,如果没有显式加锁,InnoDB会使用MVCC提供一致性视图。

每个事务在启动时都会生成一个Read View,用来确定哪些数据对当前事务可见。

其他事务在当前事务启动后插入的新数据不会被当前事务看到,因此不会出现幻读。

5.1.2、什么是当前读呢?

当前读是指读取记录的最新已提交版本,并且在读取时对记录加锁,确保其他并发事务不能修改当前记录。

比如SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE,以及UPDATE、DELETE,都属于当前读。

5.1.3、为什么UPDATE和DELETE也属于当前读?

因为更新、删除这些操作,本质上不仅是写操作,还需要在写之前读取数据,然后才能修改或删除。为了保证修改的是最新的数据,并防止并发冲突,InnoDB必须读取最新版本的数据并加锁,因此UPDATE和DELETE也属于当前读。

SQL语句 是否当前读 是否加锁
SELECT * FROM user WHERE id=1 ❌ 否 ❌ 否
SELECT * FROM user WHERE id=1 FOR UPDATE ✅ 是 ✅ 加排他锁
SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE ✅ 是 ✅ 加共享锁
UPDATE user SET ... WHERE id=1 ✅ 是 ✅ 加排他锁
DELETE FROM user WHERE id=1 ✅ 是 ✅ 加排他锁

5.1.4、什么是快照读呢?

快照读是InnoDB通过MVCC实现的一种非阻塞读方式。当事务执行SELECT查询时,InnoDB 并不会直接读当前最新的数据,而是根据事务开始时生成的Read View去判断每条记录的可见性,从而读取符合条件的历史版本。

SQL 是否快照读? 说明
SELECT * FROM t WHERE id=1 ✅ 是 快照读
SELECT * FROM t WHERE id=1 FOR UPDATE ❌ 否 当前读,读取最新版本并加锁
UPDATE / DELETE ❌ 否 当前读,必须读取当前版本并加锁
INSERT ❌ 否 写操作,不存在历史版本

6、MVCC了解吗?

MVCC指的是多版本并发控制,每次修改数据时,都会生成一个新的版本,而不是直接在原有数据上进行修改。并且每个事务只能看到在它开始之前已经提交的数据版本。

这样的话,读操作就不会阻塞写操作,写操作也不会阻塞读操作,从而避免加锁带来的性能损耗。

其底层实现主要依赖于Undo Log和Read View。

每次修改数据前,先将记录拷贝到Undo Log,并且每条记录会包含三个隐藏列,DB_TRX_ID用来记录修改该行的事务ID,DB_ROLL_PTR 用来指向Undo Log中的前一个版本,DB_ROW_ID用来唯一标识该行数据(仅无主键时生成)。

每次读取数据时,都会生成一个ReadView,其中记录了当前活跃事务的ID集合、最小事务 ID、最大事务ID等信息,通过与DB_TRX_ID进行对比,判断当前事务是否可以看到该数据版本。

6.1、请详细说说什么是版本链?

版本链是指InnoDB中同一条记录的多个历史版本,通过DB_ROLL_PTR字段将它们像链表一样串起来,用来支持MVCC的快照读。

假设有一张hero表,表中有这样一行记录,name为张三,city为帝都,插入这行记录的事务 id是80。

此时,DB_TRX_ID的值就是 80,DB_ROLL_PTR的值就是指向这条 insert undo 日志的指针。

接下来,如果有两个DB_TRX_ID分别为100、200的事务对这条记录进行了update操作,那么这条记录的版本链就会变成下面这样:

也就是说,当更新一行数据时,InnoDB不会直接覆盖原有数据,而是创建一个新的数据版本,并更新 DB_TRX_ID和DB_ROLL_PTR,使它们指向前一个版本和相关的undo日志。

这样,老版本的数据就不会丢失,可以通过版本链找到。

由于undo日志会记录每一次的 update,并且新插入的行数据会记录上一条undo日志的指针,所以可以通过DB_ROLL_PTR这个指针找到上一条记录,这样就形成了一个版本链。

6.2、请详细说说什么是ReadView?

ReadView是InnoDB为每个事务创建的一份"可见性视图",用于判断在执行快照读时,哪些数据版本是当前这个事务可以看到的,哪些不能看到。

当事务开始执行时,InnoDB会为该事务创建一个ReadView,这个ReadView会记录4个重要的信息:

(1)creator_trx_id:创建该ReadView的事务ID。

(2)m_ids:所有活跃事务的ID列表,活跃事务是指那些已经开始但尚未提交的事务。

(3)min_trx_id:所有活跃事务中最小的事务ID。它是 m_ids 数组中最小的事务ID。

(4)max_trx_id :事务ID的最大值加一。换句话说,它是下一个将要生成的事务ID。

6.3、ReadView是如何判断记录的某个版本是否可见的?

会通过三个步骤来判断:

①、如果某个数据版本的DB_TRX_ID小于min_trx_id,则该数据版本在生成ReadView之前就已经提交,因此对当前事务是可见的。

②、如果DB_TRX_ID大于max_trx_id,则表示创建该数据版本的事务在生成ReadView之后开始,因此对当前事务不可见。

③、如果DB_TRX_ID在min_trx_id和max_trx_id之间,需要判断DB_TRX_ID是否在m_ids列表中:

不在,表示创建该数据版本的事务在生成 ReadView 之后已经提交,因此对当前事务也是可见的。

在,表示事务仍然活跃,或者在当前事务生成 ReadView 之后才开始,因此是不可见的。

举个实际的例子。

读事务开启了一个ReadView,这个ReadView 里面记录了当前活跃事务的ID列表(444、555、665),以及最小事务ID(444)和最大事务ID(666)。当然还有自己的事务ID520,也就是creator_trx_id。

它要读的这行数据的写事务ID是x,也就是DB_TRX_ID。

①如果x = 110,显然在 ReadView 生成之前就提交了,所以这行数据是可见的。

②如果x = 667,显然是未知世界,所以这行数据对读操作是不可见的。

③如果x = 519,虽然519大于444小于666,但是519不在活跃事务列表里,所以这行数据是可见的。因为 519是在 520 生成 ReadView 之前就提交了。

④如果x = 555,虽然555大于444小于666,但是555在活跃事务列表里,所以这行数据是不可见的。因为 555不确定有没有提交。

6.4、可重复读和读已提交在 ReadView 上的区别是什么?

可重复读:在第一次读取数据时生成一个ReadView,这个ReadView会一直保持到事务结束,这样可以保证在事务中多次读取同一行数据时,读取到的数据是一致的。

读已提交:每次读取数据前都生成一个ReadView,这样就能保证每次读取的数据都是最新的。

6.5、如果两个AB事务并发修改一个变量,那么A读到的值是什么,怎么分析。

事务A在读取时是否能读到事务B的修改,取决于A是快照读还是当前读。如果是快照读,InnoDB会使用MVCC的ReadView判断记录版本是否可见,若事务B尚未提交或在A的视图不可见,则A会读到旧值;如果是当前读,则需要加锁,若B已提交可直接读取,否则A会阻塞直到B结束。

相关推荐
杨云龙UP2 小时前
Oracle 19c:RMAN Duplicate异机复制数据库实操_20260402
linux·运维·服务器·数据库·网络协议·tcp/ip·oracle
刘~浪地球2 小时前
Redis 从入门到精通(七):集合操作详解
数据库·chrome·redis
光泽雨2 小时前
SQL分组Group By
数据库
我真会写代码2 小时前
MySQL高频面试题(含详细解析):从基础到高级,备战面试不踩坑
数据库·mysql·面试
观远数据2 小时前
未来3年企业数据分析的核心:基于自然语言的AI优先决策体系如何搭建
数据库·人工智能·数据分析
YummyJacky2 小时前
Redis在项目中的应用
数据库·redis·缓存
qq_283720052 小时前
MySQL 8.0新特性高频面试题 30 道(超详细答案)
数据库·mysql·面试·mysql8·高频试题
wAEWQ6Ib73 小时前
[拆解LangChain执行引擎]支持自然语言查询的长期存储
数据库·oracle·langchain
吴声子夜歌3 小时前
Node.js——操作MongoDB
数据库·mongodb·node.js