0x00 前言
在MySQL的学习与实际开发中,事务隔离级别是绕不开的核心知识点。它直接决定了多事务并发场景下数据的一致性、可靠性,也是面试中高频考点。
很多时候我们只记住了READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE这四个级别,却不清楚不同级别下会出现脏读、不可重复读、幻读、丢失更新中的哪些问题,更没有亲手做过实验验证。
本篇博客AI(嗯,不想打字了,AI打字吧)将带你系统梳理MySQL事务隔离级别知识点,并通过可复现的实验操作,直观看到每一种隔离级别下的并发表现,彻底吃透这一知识点。
0x01 前置知识:事务与并发问题
1.什么是MySQL事务
事务是一组不可分割的SQL操作,满足ACID四大特性:
原子性(Atomicity):要么全部执行,要么全部回滚
一致性(Consistency):执行前后数据完整性不变
隔离性(Isolation):多个事务并发时互不干扰
持久性(Durability):提交后数据永久生效
2.事务并发带来的四大问题
在不做隔离控制时,多事务并发会出现以下问题:
- 脏读(
Dirty Read)
一个事务读到了另一个未提交事务修改的数据。
- 不可重复读(
Non-Repeatable Read)
一个事务内,同一查询多次执行,结果不一致(其他事务已UPDATE/DELETE并提交)。
- 幻读(
Phantom Read)
一个事务内,多次查询结果集行数发生变化(其他事务INSERT并提交)。
- 丢失更新(
Lost Update)
两个事务同时读取同一条数据,各自基于读到的旧数据做修改、最后先后提交,后提交的事务覆盖了前一个事务已提交的修改,导致前者更新数据凭空丢失。
0x02 MySQL四大事务隔离级别
MySQL InnoDB引擎支持四种隔离级别,从上到下隔离程度越来越高,并发性能越来越低:

默认级别:MySQL InnoDB默认使用REPEATABLE READ(可重复读)。
0x03 实验准备
1.环境说明
数据库:MySQL 5.5+ / 8.0+ 均可
存储引擎:InnoDB(必须,支持事务)
工具:两个终端窗口(模拟事务 A、事务 B 并发)
2.建表与初始化数据
创建一张score表,用于实验:
sql
-- 创建成绩表 score
CREATE TABLE score (
sid INT,
cid VARCHAR(10),
score INT,
PRIMARY KEY (sid, cid)
);
-- 插入成绩数据
INSERT INTO score VALUES
(101, '001', 85),
(101, '002', 92),
(102, '001', 78),
(102, '002', 88),
(103, '001', 90),
(103, '003', 82),
(104, '002', 75),
(105, '003', 95);
3.事务常用命令
sql
-- 开启事务
START TRANSACTION; # BEGIN; 也可以更加简单省事
-- 提交事务
COMMIT;
--- 设置回滚点
SAVEPOINT sp_name;
-- 回滚事务
ROLLBACK;
ROLLBACK TO sp_name;
-- 查看当前隔离级别
SELECT @@tx_isolation;
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL [级别];
0x04 实战实验:逐级别验证
我们将依次在四个隔离级别下,验证脏读、不可重复读、幻读、丢失更新。
约定:
-
左侧窗口 = 事务
A -
右侧窗口 = 事务
B
实验 1:READ UNCOMMITTED(读未提交)
特点:最低级别,能读到未提交数据,会出现脏读。
步骤 1:两个窗口都设置隔离级别

步骤 2:开启事务 A、B
sql
-- 事务A
BEGIN;
-- 事务B
BEGIN;
步骤 3:事务 A 修改数据,不提交
sql
MariaDB [test0604]> begin;
Query OK, 0 rows affected (0.000 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.001 sec)
MariaDB [test0604]> update score set score=96 where sid=105;
Query OK, 1 row affected (0.020 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 96 |
+-----+-----+-------+
8 rows in set (0.000 sec)
步骤 4:事务 B 查询数据
sql
MariaDB [test0604]> begin;
Query OK, 0 rows affected (0.000 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 96 |
+-----+-----+-------+
8 rows in set (0.000 sec)
结果:
事务 B 读到了score分数改为96,但事务 A根本没提交,出现脏读。
实验 2:READ COMMITTED(读已提交)
特点:只能读到已提交数据,解决脏读,但存在不可重复读和幻读。
步骤 1:两个窗口重新设置隔离级别后开启事务

步骤 2:事务 A 修改数据,不提交
sql
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.001 sec)
MariaDB [test0604]> update score set score=96 where sid=105;
Query OK, 1 row affected (0.010 sec)
Rows matched: 1 Changed: 1 Warnings: 0
步骤 3:事务 B 查看数据,已不存在脏读问题
sql
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.000 sec)
步骤 4:事务 A提交事务后,事务B再次查看

结论:可以发现事务B中出现了不可重复读问题,同理,如果在步骤2中用的insert或者delete语句则会出现幻读问题。
实验 3:REPEATABLE READ(可重复读)
特点:MySQL默认级别,解决脏读、不可重复读,InnoDB 还解决了幻读。
步骤 1:两个窗口重新设置隔离级别后开启事务

步骤 2:事务 A 修改数据,不提交
sql
MariaDB [test0604]> begin;
Query OK, 0 rows affected (0.000 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.001 sec)
MariaDB [test0604]> delete from score where sid=105;
Query OK, 1 row affected (0.008 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
+-----+-----+-------+
7 rows in set (0.000 sec)
步骤 3:事务 B 数据无变化,无脏读问题
步骤 4:事务 A 提交
sql
MariaDB [test0604]> commit;
Query OK, 0 rows affected (0.001 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
+-----+-----+-------+
7 rows in set (0.001 sec)
步骤 5:事务 B 数据仍然无变化,无幻读问题(当然也没不可重复读问题)
sql
MariaDB [test0604]> begin;
Query OK, 0 rows affected (0.000 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.002 sec)
-- 事务A提交后仍然无变化
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.000 sec)
步骤 6:事务 B 在保持不修改的情况下提交事务,并再次进行查看
sql
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
| 105 | 003 | 95 |
+-----+-----+-------+
8 rows in set (0.000 sec)
MariaDB [test0604]> commit;
Query OK, 0 rows affected (0.000 sec)
MariaDB [test0604]> select * from score;
+-----+-----+-------+
| sid | cid | score |
+-----+-----+-------+
| 101 | 001 | 85 |
| 101 | 002 | 92 |
| 102 | 001 | 78 |
| 102 | 002 | 88 |
| 103 | 001 | 90 |
| 103 | 003 | 82 |
| 104 | 002 | 75 |
+-----+-----+-------+
7 rows in set (0.000 sec)
发现事务B在没有做任何操作的情况下score的记录数少了一条,从侧面说明会发生丢失更新的情况(事务前有8条记录,事务后现在只有7条了)。这里又使用了另外一种办法进行验证,详情请看下图。

结论:发现虽然可以解决脏读,不可重复读,幻读问题,但是仍然会出现丢失更新的问题。
实验 4:SERIALIZABLE(串行化)
步骤 1:两个窗口重新设置隔离级别后开启事务

步骤 2:两个窗口一起更新

事务A中的查询时间很长,是因为事务B对数据加锁了,后来在事务B中也进行操作,就出现了死锁问题,导致了事务的重启,事务A才在20多秒以后才能完成数据的更新。可以发现事务A 会阻塞等待,直到 B 事务完成。
结论:SERIALIZABLE:完全安全,但并发性能极差。