目录
3.3.1 读未提交【Read Uncommitted】测试
在日常的后端开发中,数据库的并发访问是一个无法回避的问题。以火车票售票系统为例:当客户端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;
quit
SELECT @@transaction_isolation;

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)
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)
-- 终端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 UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE。
-- 查看全局隔离级别
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 幻读 和 不可重复读的区别
在一个事务内,当执行两次完全相同的 范围查询时,第二次查询的结果集 行数与第一次不同,多出了几行“幻影”般的新数据。这多出的新数据,是由其他事务在两次查询的间隙中 执行插入(INSERT)操作并提交所导致的。
(2)幻读关键特征:
-
操作类型:幻读问题严格由新数据的插入(INSERT)引起。数据被修改(UPDATE)或删除(DELETE)导致的数据变化,属于“不可重复读”的范畴。
-
影响范围:它改变的是一个“集合”的行数。当你用一个
WHERE条件锁定了一个范围的数据时,另一个事务可以在这个范围里“悄无声息”地插入一条满足条件的新记录。 -
隐蔽性:与修改已有数据不同,新插入的数据并不存在一个可以被第一事务发现的、被“修改”的状态。它直接就从“不存在”变成了“存在”,像一个幽灵,防不胜防。
| 对比维度 | 不可重复读 | 幻读 |
|---|---|---|
| 核心操作 | UPDATE 或 DELETE | INSERT |
| 数据库表现 | 同一行数据的内容(字段值)被改变了,或者这行数据不见了。 | 结果集里凭空多出了一行或多行之前不存在的数据。 |
| 影响范围 | 针对特定的、已存在的行。 | 针对一个满足查询条件的行集合。 |
| 读操作 | 多次读取同一行,发现值不同。 | 多次执行同一范围查询,发现行数不同。 |
| 解决方法 | 在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)
4.2 Undo Log与版本链
4.2.1 模拟 MVCC
4.2.2 一些思考
2.如果是 insert 呢?
因为 insert 是插入,也就是之前没有数据,那么 insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务 commit 了,那么这个 undo log 的历史insert记录就可以被清空了。
总结一下,也就是我们可以理解成,update 和 delete 可以形成版本链,insert 暂时不考虑。
3. 那么 select 呢?
4.2.3 总结
-
每次更新记录前,都会将当前记录写入
undo log作为历史快照。 -
新记录的
DB_ROLL_PTR指向上一个版本,形成一条版本链。 -
INSERT操作因无历史数据,为支持回滚也会写入undo log,但提交后可被清理(可反向执行delete操作)。 -
DELETE操作并非物理删除,而是标记删除flag,同样会形成历史版本。
4.3 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;
// 省略...
};
对应源码:

可见性判定流程(当需要判断版本链中某个版本的 DB_TRX_ID 是否可见时):
-
若
DB_TRX_ID == creator_trx_id(是当前事务自己修改的)→ 可见。 -
若
DB_TRX_ID < up_limit_id(在视图创建前已提交)→ 可见。 -
若
DB_TRX_ID >= low_limit_id(在视图创建后才开始)→ 不可见。 -
若
up_limit_id <= DB_TRX_ID < low_limit_id,则检查DB_TRX_ID是否在活跃列表m_ids中:-
若在,说明生成视图时该事务仍未提交 → 不可见。
-
若不在,说明生成视图时该事务已提交 → 可见。
-
若当前版本不可见,则沿DB_ROLL_PTR指针访问上一历史版本,继续判断,直到找到可见版本。





4.5 RR与RC的本质区别
-- 终端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)

-- 终端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)

所以RR与RC的本质区别 这正是与 ReadView生成时机不同导致的:
-
READ COMMITTED(RC):每次快照读都会生成一个全新的ReadView。因此,一个事务内多次读取能看到其他事务提交的更新,导致“不可重复读”。 -
REPEATABLE READ(RR):仅在事务内第一次快照读时生成一个ReadView,后续所有快照读都复用它。只要第一次读时某事务未提交,其后即使它提交了,在本事务中也永远不可见,从而实现了“可重复读”。
297

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



