深入理解MySQL事务隔离级别:从理论到实战实验

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:完全安全,但并发性能极差。

相关推荐
geovindu1 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
for_ever_love__1 小时前
MySQL SELECT 与 WHERE 优化实战:可搜索条件、联合索引与执行计划
数据库·mysql·oracle·select·where·条件优化·查询优化技巧
2501_931803752 小时前
MySQL 锁机制详解
数据库·mysql
.Hypocritical.5 小时前
MySQL |常用 SQL 语句与 pymysql 函数
mysql
NineData5 小时前
MySQL 到 Doris 实时数据同步:订单、用户和日志数据入仓方案
数据库·mysql
Dovis(誓平步青云)6 小时前
现场问题先关闭还是先验收,建筑任务怎样处理依赖
前端·javascript·数据库·mysql·音视频
xuefuhe7 小时前
MySQL:group_replication_start_on_boot vs group_replication_bootstrap_group
mysql
for_ever_love__8 小时前
MySQL Online DDL 完全指南:INSTANT/INPLACE/COPY 三种算法、MDL 锁事故与 gh-ost 实战
数据库·mysql·ddl·online·gh-ost·锁表·加字段
for_ever_love__8 小时前
MySQL 子查询、派生表与 CTE 优化:半连接、物化和递归实战
数据库·mysql·优化·as·子查询·with·cte
鱼很腾apoc10 小时前
【MySQL】 第1期 从核心概念到表约束全解析
linux·c语言·数据库·学习·mysql