数据世界的“契约精神”:MySQL 事务与并发控制深度解析

        

目录

一、什么是事务?

二、事务的基本操作

2.1. 提交方式

2.2 核心操作命令

2.3 实验示例

2.4 结论

三、事务隔离级别

3.1 并发读的三大问题

3.2 四种隔离级别

3.3 查看与设置隔离级别

3.3.1 读未提交【Read Uncommitted】测试

3.3.2 读已提交【Read Committed】测试

3.3.3 可重复读【Repeatable Read】测试

3.3.3.1 幻读 和 不可重复读的区别

3.3.4 串行化【serializable】

3.4 总结

3.5 补充 : 一致性

四、深入理解隔离性:MVCC原理

4.1 三个关键隐藏字段

4.1.1 读-写

4.2 Undo Log与版本链

4.2.1 模拟 MVCC

4.2.2 一些思考

4.2.3 总结

4.3 ReadView:可见性判断的核心

4.5 RR与RC的本质区别

        在日常的后端开发中,数据库的并发访问是一个无法回避的问题。以火车票售票系统为例:当客户端A发现还有1张余票并决定卖出,但在它还未更新数据库时,客户端B也读取到了这1张余票并同样执行了卖票操作。最后A和B都将数据更新回数据库,就会导致“同一张票被卖了两次”的严重并发问题。

为了在极高复杂度和大数据量下解决这类并发错误,MySQL 引入了核心机制——事务(Transaction)

一、什么是事务?

事务(Transaction)是一组DML(数据操作语言)语句的集合,它们在逻辑上具有相关性。这个集合是一个不可分割的工作单位,其中的语句要么全部成功,要么全部失败。事务被设计出来的本质,是为了简化应用层的编程模型,让我们在写代码时不需要再去时刻提防网络异常、服务器宕机或并发修改带来的潜在问题。

事务主要用于处理操作量大、复杂度高的数据。一个经典例子是:毕业时,教务系统需要删除你的全部信息,这包括基本信息、各科成绩、论坛发帖等。这样,就需要多条 MySQL 语句构成,那么所有这些操作合起来就构成一个事务,以保证数据的一致性。

为了让事务能正确工作,它必须满足四个基本属性,即 ACID

属性全称核心解读
原子性Atomicity一个事务中的所有操作,要么全部完成,要么全部不完成,不会停在中间环节。如果执行中出错,会回滚(Rollback) 到事务开始前的状态。
一致性Consistency事务开始前和结束后,数据库的完整性约束没有被破坏。数据必须符合所有预设规则(如字段类型、唯一性等)。技术上通过AID保证。
隔离性Isolation数据库允许多个并发事务同时操作数据,隔离性可以防止多个事务并发执行时因交叉执行而导致数据不一致。
持久性Durability事务一旦提交(Commit),其对数据的修改就是永久的,即使后续发生系统故障也不会丢失。

重要前提:在MySQL中,只有InnoDB引擎支持事务,而MyISAM等引擎不支持。可以通过 SHOW ENGINES;命令查看各引擎对事务的支持情况。

mysql> show engines \G;
*************************** 1. row ***************************
      Engine: ARCHIVE
     Support: YES
     Comment: Archive storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 2. row ***************************
      Engine: BLACKHOLE
     Support: YES
     Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 3. row ***************************
      Engine: MRG_MYISAM
     Support: YES
     Comment: Collection of identical MyISAM tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 4. row ***************************
      Engine: FEDERATED
     Support: NO
     Comment: Federated MySQL storage engine
Transactions: NULL
          XA: NULL
  Savepoints: NULL
*************************** 5. row ***************************
      Engine: MyISAM
     Support: YES
     Comment: MyISAM storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 6. row ***************************
      Engine: PERFORMANCE_SCHEMA
     Support: YES
     Comment: Performance Schema
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 7. row ***************************
      Engine: InnoDB
     Support: DEFAULT
     Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
          XA: YES
  Savepoints: YES
*************************** 8. row ***************************
      Engine: MEMORY
     Support: YES
     Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 9. row ***************************
      Engine: CSV
     Support: YES
     Comment: CSV storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
9 rows in set (0.00 sec)

二、事务的基本操作

2.1. 提交方式

MySQL事务的提交方式有两种:

  • 自动提交autocommit = ON (默认)

  • 手动提交autocommit = OFF 

可以通过以下命令查看和修改:

-- 查看提交方式
SHOW VARIABLES LIKE 'autocommit';

-- 设置为手动提交
SET AUTOCOMMIT = 0;

-- 设置为自动提交
SET AUTOCOMMIT = 1;

2.2 核心操作命令

-- 1. 开启事务。两者功能等价,执行后不受自动提交设置影响
START TRANSACTION;
-- 或
BEGIN;

-- 2. 创建保存点(Savepoint),用于部分回滚
SAVEPOINT savepoint_name;

-- 3. 回滚到指定保存点
ROLLBACK TO savepoint_name;

-- 4. 回滚到事务开始时的状态
ROLLBACK;

-- 5. 提交事务,将所有修改持久化
COMMIT;

注意:如果一个事务还未 commit 客户端就异常崩溃了,MySQL 会自动触发回滚操作,保证数据安全。

2.3 实验示例

1. 为了便于演示,我们将mysql的默认隔离级别设置成读未提交 , 具体操作我们后面专门会讲,现在以使用为主

set global transaction isolation level READ UNCOMMITTED;
2. 需要重启终端,进行查看
quit
SELECT @@transaction_isolation;

3. 创建测试表
mysql> create table if not exists account(
    -> id int primary key,
    -> name varchar(50) not null default '',
    -> blance decimal(10,2) not null default 0.0
    -> )engine=innodb default charset=utf8;
Query OK, 0 rows affected, 1 warning (0.03 sec)
  4. 正常演示 - 证明事务的开始与回滚 
mysql> show variables like 'autocommit';     -- 查看事务是否自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

mysql> start transaction;                     -- 开启一个事务,使用begin命令也可
Query OK, 0 rows affected (0.00 sec)
 
mysql> savepoint save1;                       -- 创建一个保存点save1
Query OK, 0 rows affected (0.00 sec) 

mysql> insert into account values(1,'张三',100);   -- 插入一条记录
Query OK, 1 row affected (0.00 sec)

mysql> savepoint save2;                            -- 创建一个保存点save2
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values(2,'李四',1000);   -- 再插入一条记录
Query OK, 1 row affected (0.00 sec)

mysql> select * from account;                       -- 两条记录都存在了
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> rollback to save2;                          -- 回滚到保存点save2
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;                      -- 一条记录没有了
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> rollback;                                  -- 直接rollback,回滚到最开始
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;              -- 所有刚刚的记录都没有了
Empty set (0.00 sec)

5. 非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
 
-- 终端1
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 插入数据
mysql> insert into account values(1,'张三',100);
Query OK, 1 row affected (0.00 sec)

-- 查询
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

-- 终端2
mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

-- 终端1 异常退出 CTRL + \
mysql> Aborted

-- 终端2 查询
mysql> select * from account;
Empty set (0.00 sec)


6. 非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化

-- 终端1
mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

-- 表示当前表内没数据
Database changed
mysql> select * from account;
Empty set (0.00 sec)

-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 插入数据
mysql> insert into account values(1,'张三',100);
Query OK, 1 row affected (0.00 sec)

-- 提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

-- CTRL + \ 异常退出
mysql> Aborted

-- 终端2 能够查询到,因为已经持久化
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

7. 非正常演示3 - 对比试验 : 证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响

-- 终端1 查看目前表内数据
mysql> select *from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

-- 查看提交方式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

-- 设置取消自动提交
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 插入数据
mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.00 sec)

-- 查看表内全部数据
mysql> select *from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端2 查看数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 再次崩溃退出
mysql> Aborted

-- 终端2 查看数据 : 终端1 崩溃后数据自动回滚 
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)


8. 非正常演示4 - 证明单条 SQL 与 事务的关系 (注意这里没有开启事务)

-----------------------------实验1-----------------------------------------
-- 终端1 查看目前的数据
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

-- 查看目前的提交状态
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

-- 取消自动提交
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

-- 查看
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

-- 插入一条数据
mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.00 sec)

-- 查询数据
mysql> select *from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端2 查询
mysql> select *from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
 
-- 终端1 退出 CTRL + D

mysql> ^DBye

-- 终端2 查询数据
mysql> select *from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

------------------------------实验2----------------------------------------------

-- 终端1 自动开启自动提交
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

-- 查询初始数据
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

-- 插入数据
mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)


-- 已经插入
mysql> select *from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


-- 终端2 查询数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 异常退出
mysql> ^DBye

-- 终端2 还是能查到两条数据: 终端1 退出后数据持久化
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

2.4 结论

  • 一旦执行 BEGIN 或 START TRANSACTION,就必须通过 COMMIT 来持久化数据,此时autocommit 设置被忽略。

  • 如果客户端崩溃,MySQL会自动回滚未提交的事务(原子性与持久性)。

  • 没有设置保存点,也可以直接使用  ROLLBACK 回滚整个事务。

  • 如果事务已经 COMMIT,则无法再 ROLLBACK

三、事务隔离级别

隔离性是事务最核心的特性之一。当多个事务并发操作同一张表甚至同一行数据时,就可能出现各种并发问题。

3.1 并发读的三大问题

  • 脏读(Dirty Read):一个事务读到了另一个未提交事务修改过的数据。

  • 不可重复读(Non-repeatable Read):在同一个事务内,多次读取同一数据,结果不同。重点在于修改和删除操作。

  • 幻读(Phantom Read):在同一个事务内,多次执行同条件查询,结果集的行数不同,仿佛出现了幻觉。重点在于新增操作。

3.2 四种隔离级别

隔离级别脏读不可重复读幻读实现特点
读未提交 (Read Uncommitted)几乎不加锁,并发最高,但问题最多,生产绝对不用
读已提交 (Read Committed)×大多数据库默认级别(非MySQL),存在不可重复读问题。
可重复读 (Repeatable Read)×××(注1)MySQL InnoDB默认级别。通过MVCC和锁机制解决了幻读。
串行化 (Serializable)×××强制事务排序,锁竞争激烈,效率极低,基本不用。

注1:在标准的SQL规范中,可重复读隔离级别无法完全解决幻读,但InnoDB引擎通过Next-Key Lock(间隙锁+行锁) 机制,基本解决了幻读问题。

3.3 查看与设置隔离级别

-- 查看全局隔离级别
SELECT @@global.transaction_isolation;

-- 查看当前会话隔离级别
SELECT @@session.transaction_isolation;
-- 或
SELECT @@transaction_isolation;

-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局隔离级别(对后续新会话生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

可选级别:READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE

-- 查看全局隔离级别
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED               |
+--------------------------------+
1 row in set (0.00 sec)

-- 查看会话(当前)隔离级别
mysql> select @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| READ-UNCOMMITTED                |
+---------------------------------+
1 row in set (0.00 sec)

-- 默认同上
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |
+-------------------------+
1 row in set (0.00 sec)

--设置

-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}

-- 设置为串行化
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)


-- 查看
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE            |
+-------------------------+
1 row in set (0.01 sec)

-- 全局还是READ-UNCOMMITTED 
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED               |
+--------------------------------+
1 row in set (0.00 sec)

-- 当前会话  SERIALIZABLE  
mysql> select @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| SERIALIZABLE                    |
+---------------------------------+
1 row in set (0.00 sec)

--设置全局隔离性,另起一个会话(终端),会被影响

mysql> SELECT @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED               |
+--------------------------------+
1 row in set (0.00 sec)

mysql> SELECT @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| READ-UNCOMMITTED                |
+---------------------------------+
1 row in set (0.00 sec)

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |
+-------------------------+
1 row in set (0.00 sec)

3.3.1 读未提交【Read Uncommitted】测试

几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用
-- 终端1 设置全局隔离级别
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

-- 退出客户端重新进入
mysql> quit
Bye
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 36
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

-- 查看当前隔离级别
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |
+-------------------------+
1 row in set (0.00 sec)

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 查看目前表中数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 更新数据
mysql> update account set blance = 123.0 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

---- 注意: 未commit

-- 终端2 
mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)


-- 查询数据 : 终端2查到了终端1没有commit的数据

--一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读
(dirty read)


mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   123.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

3.3.2 读已提交【Read Committed】测试

-- 终端1 设置全局隔离级别
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

-- 退出重启客户端
mysql> quit
Bye
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 42
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 查看当前数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   123.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 更新数据
mysql> update account set blance = 321.0 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- 终端1 目前的数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 开启终端2
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 43
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 终端2开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 终端2查看数据 -- 老的数据 
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   123.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 commit !
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 查看数据 新的值
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- but,此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段
(依旧还在事务操作中!) 读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!

3.3.3 可重复读【Repeatable Read】测试

-- 终端1 设置隔离级别为可重复读
mysql> set global transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

-- 退出重启
mysql> quit
Bye
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 44
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 查看目前隔离级别
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

-- 查看当前数据
mysql> select *from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 开启事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)

-- 打开终端2 同时开启事务
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 45
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 修改数据
mysql> update account set blance=4321.0 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- 终端2 查看 数据未改变
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 commit
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

-- 终端2 再次查看 数据还是没有发生改变
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端2 commit
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 再次查看数据 发现数据已经更新!
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 可以看到,在终端2中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
-- 如果将上面的终端A中的update操作,改成insert操作,会有什么问题?
-- 终端1 查看数据
mysql> select *from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 终端1 插入新数据并查看
mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
Query OK, 1 row affected (0.00 sec)

mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

-- 终端2 查看数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端2 commit 之后查看数据
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端2 再次开启事务 然后查看
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

-- 终端1 commit
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 查询 还是旧数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

多次查看,发现终端1在对应事务中insert的数据,
在终端2的事务周期中,也没有什么影响,也符合可重复的特点。
但是一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据
(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,
那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,
但是insert的数据在可重复读情况被读取出来,
导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。
这种现象,叫做幻读(phantom read)。

很明显,MySQL在RR级别的时候,是解决了幻读问题的,没有查到其他事务新插入的数据!!!
它是如何打破“RR级别存在幻读”这个传统数据库理论的?答案是:Next-Key Lock(临键锁)。
Next-Key Lock = 行锁 (Record Lock) + 间隙锁 (Gap Lock)

-- 终端2 commit
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 中才查到了最新的数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
3.3.3.1 幻读 和 不可重复读的区别
(1)幻读核心定义:
在一个事务内,当执行两次完全相同的 范围查询时,第二次查询的结果集 行数与第一次不同,多出了几行“幻影”般的新数据。这多出的新数据,是由其他事务在两次查询的间隙中 执行插入(INSERT)操作并提交所导致的。

(2)幻读关键特征:

  1. 操作类型:幻读问题严格由新数据的插入(INSERT)引起数据被修改(UPDATE)或删除(DELETE)导致的数据变化,属于“不可重复读”的范畴。

  2. 影响范围:它改变的是一个“集合”的行数。当你用一个 WHERE 条件锁定了一个范围的数据时,另一个事务可以在这个范围里“悄无声息”地插入一条满足条件的新记录。

  3. 隐蔽性:与修改已有数据不同,新插入的数据并不存在一个可以被第一事务发现的、被“修改”的状态。它直接就从“不存在”变成了“存在”,像一个幽灵,防不胜防。

对比维度不可重复读幻读
核心操作UPDATE 或 DELETEINSERT
数据库表现同一行数据的内容(字段值)被改变了,或者这行数据不见了。结果集里凭空多出了一行或多行之前不存在的数据。
影响范围针对特定的、已存在的针对一个满足查询条件的行集合
读操作多次读取同一行,发现值不同。多次执行同一范围查询,发现行数不同。
解决方法READ COMMITTED (RC) 级别下就会发生,通过REPEATABLE READ (RR) 行锁即可解决。REPEATABLE READ (RR) 级别下(如果没特殊处理)会发生,需要通过更复杂的间隙锁来解决。
举例读了张三的年龄是20,一会儿再读变成了21岁。第一次查询1班有1个学生,第二次查询1班有2个学生。

3.3.4 串行化【serializable】

对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用

-- 终端1 设置串行化隔离级别
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

mysql> quit
Bye
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 50
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE            |
+-------------------------+
1 row in set (0.00 sec)

-- 终端1 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 终端2 查询数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

-- 终端1 查询数据
mysql> select * from account;             -- 两个读取不会串行化
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

-- 终端1 更新数据 : 注意此时终端1 的这个操作会阻塞,因为终端2 的事务还没有结束(commit)
mysql> update account set blance = 1234.0 where id = 1;

-- 终端2 commit
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端1 结束阻塞
mysql> update account set blance = 1234.0 where id = 1;
Query OK, 1 row affected (29.95 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- 终端2 进行查询还是之前的数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  4321.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

--终端1 commit并查询
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  1234.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
 
-- 终端2 查询数据 -- 这时才查到了更新之后的数据
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |  1234.00 |
|  2 | 李四   | 10000.00 |
|  3 | 王五   |  5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

3.4 总结

事务隔离级别是并发性能与数据安全性之间的权衡艺术。

  • 安全性:隔离级别越严格(如串行化),数据越安全,但并发性能越低。

  • 并发性:隔离级别越宽松(如读未提交),并发性能越高,但数据问题越多。

  • 最佳实践:工程中需在两者间找到平衡点。MySQL默认的可重复读(REPEATABLE READ) 是一个兼顾性能与安全的成熟选择,一般不建议修改。

最后,牢记两种易混淆的并发读现象核心区别:

  • 不可重复读:重点是 UPDATE/DELETE。同一行数据,前后读到的内容(值)不一样。

  • 幻读:重点是 INSERT。同一条件查询,前后读到的行数(记录数)不一样。

3.5 补充 : 一致性

一致性(Consistency):ACID的最终目标

一致性是事务追求的最终目标——数据库必须从一个正确的状态,迁移到另一个正确的状态。这个“正确”不仅仅是数据库层面不违反约束,更重要的是满足业务逻辑的完整性。

可以这样理解:

  • 业务层面:一致性由用户和业务逻辑定义。例如,转账操作中“总额不变”就是一条业务规则。

  • 技术层面:MySQL通过原子性(A)、隔离性(I)、持久性(D) 三大特性,共同为一致性保驾护航。一旦系统中断,未完成的事务会被原子性地回滚,确保数据库不会停留在不一致的中间状态。

四、深入理解隔离性:MVCC原理

如何理解隔离性?

隔离性主要通过多版本并发控制(MVCC) 实现。MVCC解决“读-写”冲突,实现无锁并发。

MVCC 可以为数据库解决以下问题:

1. 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。

2. 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

理解 MVCC 需要知道三个前提知识3个记录隐藏字段 undo log Read View , 下面我们一一介绍。

4.1 三个关键隐藏字段

数据库并发的场景有三种:
读-读 :不存在任何问题,也不需要并发控制。
读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。
写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。

4.1.1 读-写

InnoDB为每行记录都维护了3个重要隐藏字段:

  • DB_TRX_ID (6字节) :最近修改(插入/更新)本行数据的事务ID。(每个事务都要有自己的事务ID可以根据事务ID的大小,来决定事务到来的先后顺序 。mysqld可能会面临处理多个事务的情况,事务也有自己的生命周期,所以mysqld要对多个事务进行管理,事务在我看来,mysqld中一定是对应的一个或者一套结构体对象/类对象,事务也要有自已的结构体。
    )

  • DB_ROLL_PTR (7字节) :回滚指针,指向undo log中本行记录的上一个版本。

  • DB_ROW_ID (6字节) :隐含的自增ID。如果表未定义主键,InnoDB会用它自动生成聚簇索引。

  • 补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了。

假设测试表结构是:

mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name | age |
+--------+-----+
| 张三 | 28 |
+--------+-----+
1 row in set (0.00 sec)
实际mysql建立的:
我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1。第一条记录也没有其他版本,我们设置回滚指针为null。

4.2 Undo Log与版本链

有一件事情得明白, MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。

所以,我们这里理解undo log,就简单理解成 : 就是 MySQL 中的一段内存缓冲区用来保存日志数据的就行。

4.2.1 模拟 MVCC

操作: 现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四) , 系统中将完成:
1. 事务10,因为要修改,所以要先给该记录加行锁。
2. 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写
时拷贝)
3. 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 '李四'。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
4. 事务 10 提交,释放锁。
操作 : 现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)。系统中将完成:
1. 事务11,因为也要修改,所以要先给该记录加行锁。(为最新的李四的那条记录)
2. 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log。
3. 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
4. 事务11提交,释放锁。
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据去覆盖当前数据。 上面的一个一个版本,我们可以称之为一个一个的快照

4.2.2 一些思考

1. 上面是以 update 主讲的,如果是 delete 呢?
一样的,别忘了,删数据不是清空,而是设置 flag 为删除即可 , 也可以形成版本。

2.如果是 insert 呢?

因为 insert 是插入,也就是之前没有数据,那么 insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务 commit 了,那么这个 undo log 的历史insert记录就可以被清空了。

总结一下,也就是我们可以理解成,update 和 delete 可以形成版本链,insert 暂时不考虑。

3. 那么 select 呢?

首先 select 不会对数据做任何修改,所以,为 select 维护多版本,没有意义。不过,此时有个问题就是select读取,是读取最新的版本呢?还是读取历史版本?select分为: 
当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in share mode(共享锁), select for update 命令
快照读:读取历史版本(一般而言),就叫做快照读。(这个我们后面重点讨论)
我们可以看到,在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有 select 过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。 但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率,即MVCC的意义所在。
那么,是什么决定了,select是当前读,还是快照读呢?当然是隔离级别!
那为什么要有隔离级别呢?因为事务都是原子的。所以,无论如何,事务总有先有后。所以我们要实现数据的隔离就可以通过版本的隔离来实现。
但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但是不管怎么启动多个事务,总是有先有后的。
那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
先来的事务,应不应该看到后来的事务所做的修改呢?
那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?我们接下来探究。

4.2.3 总结

  • 每次更新记录前,都会将当前记录写入undo log作为历史快照。

  • 新记录的 DB_ROLL_PTR 指向上一个版本,形成一条版本链

  • INSERT操作因无历史数据,为支持回滚也会写入 undo log,但提交后可被清理(可反向执行delete操作)。

  • DELETE 操作并非物理删除,而是标记删除flag,同样会形成历史版本。

4.3 ReadView:可见性判断的核心

Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
下面是 ReadView 结构,我们简化一下:
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;

/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;

/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;

/** 创建视图时的活跃事务id列表*/ -- 类似集合类型
ids_t m_ids;

-- purge 一个线程 配合 undo log 解决刷新等问题
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/

trx_id_t m_low_limit_no;

/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
重要的字段:
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的
DB_TRX_ID 。
那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的
DB_TRX_ID 。
所以现在的问题就是,当前快照读,应不应该读到当前版本记录。一张图,解决所有问题!

对应源码:

可见性判定流程(当需要判断版本链中某个版本的 DB_TRX_ID 是否可见时):

  1. 若 DB_TRX_ID == creator_trx_id(是当前事务自己修改的)→ 可见

  2. 若 DB_TRX_ID < up_limit_id(在视图创建前已提交)→ 可见

  3. 若 DB_TRX_ID >= low_limit_id(在视图创建后才开始)→ 不可见

  4.  up_limit_id <= DB_TRX_ID < low_limit_id,则检查 DB_TRX_ID 是否在活跃列表 m_ids 中:

    • 若在,说明生成视图时该事务仍未提交 → 不可见

    • 若不在,说明生成视图时该事务已提交 → 可见

若当前版本不可见,则沿DB_ROLL_PTR指针访问上一历史版本,继续判断,直到找到可见版本。

4.5 RR与RC的本质区别

4.5.1 当前读和快照读在RR级别下的区别
select * from user lock in share mode ,以加共享锁方式进行读取,对应的就是当前读。此
处只作为测试使用。
测试用例1-表1:
-- 终端1 设置RR模式下测试
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

-- 重启终端
mysql> quit
Bye
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 57
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

-- 查看当前表中数据
mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   15 |
+----+--------+------+
1 row in set (0.00 sec)
-- 查看隔离级别
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

-- 终端2
root@VM-8-17-ubuntu:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 58
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use transaction_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

-- 两个终端同时 begin 开启事务

-- 终端1 
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   15 |
+----+--------+------+
1 row in set (0.00 sec)

-- 终端2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;  -- 形成Read View 
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   15 |
+----+--------+------+
1 row in set (0.00 sec)

-- 终端1修改数据
mysql> update user set age = 18 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   18 |
+----+--------+------+
1 row in set (0.00 sec)

-- 终端2 查看数据 -还是旧数据
mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   15 |
+----+--------+------+
1 row in set (0.00 sec)

-- 终端1 commit
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 查看数据 -还是旧数据: 因为2还没有commit 符合RR级别的预期
mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   15 |
+----+--------+------+
1 row in set (0.00 sec)

-- 终端2 把当前的快照读提升为当前读,
-- 所以可以读到终端1commit之后的数据
mysql> select * from user lock in share mode;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   18 |
+----+--------+------+
1 row in set (0.00 sec)

-- 终端2 commit 查看数据 - 为最新数据
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   18 |
+----+--------+------+
1 row in set (0.00 sec)


测试用例2-表2:
-- 终端1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 终端2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

-- 终端1 
mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   18 |
+----+--------+------+
1 row in set (0.00 sec)

mysql> update user set age=28 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- 终端2 也符合RR级别预期
-- 因为事务1已经提交了,我事务2自然能看到,但是我没有在事务1 
-- commit之前形成快照产生Read View,所以没有得到旧数据 
-- 所以我这次select形成的就是事务1提交之后的快照
-- 即数据改动了新的数据可以被我看到
mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   28 |
+----+--------+------+
1 row in set (0.00 sec)

mysql> select * from user lock in share mode
    -> ;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   28 |
+----+--------+------+
1 row in set (0.00 sec)

用例1与用例2:唯一区别仅仅是 表1 的事务2 在事务 1 修改age前 快照读(select) 过一次age数据
而 表2 的事务2在事务1修改age前没有进行过快照读。
结论:
        事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力。delete也是同样如此。

所以RR与RC的本质区别 这正是与 ReadView生成时机不同导致的:

  • READ COMMITTED (RC)每次快照读都会生成一个全新ReadView。因此,一个事务内多次读取能看到其他事务提交的更新,导致“不可重复读”。

  • REPEATABLE READ (RR):仅在事务内第一次快照读时生成一个ReadView,后续所有快照读都复用它。只要第一次读时某事务未提交,其后即使它提交了,在本事务中也永远不可见,从而实现了“可重复读”。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值