深入理解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:开启事务 AB

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

相关推荐
九皇叔叔10 小时前
《MySQL 体系架构详解:Server 层、SQL 执行流程与 InnoDB、MyISAM、MEMORY 存储引擎》
sql·mysql·架构
愤怒的苹果ext13 小时前
MySQL Shell备份恢复数据库
数据库·mysql·备份恢复·mysqlsh
超兔一体云16 小时前
MySQL索引优化实战——从慢查询到索引调优
后端·mysql
2601_9620748116 小时前
大数据-264 实时数仓 - Canal MySQL的binlog研究 存储目录 变动信息 配置MySQL
大数据·数据库·mysql
2601_9620738117 小时前
大数据-240 离线数仓 - 广告业务 测试 ADS层数据加载 DataX数据导出到 MySQL
大数据·数据库·mysql
这个DBA有点耶19 小时前
SQL:2023新特性详解:JSON_TABLE、GREATEST/LEAST、QUALIFY 到底怎么用?
数据库·sql·mysql
小王C语言19 小时前
MySQL 用户管理:创建用户、给用户授权
数据库·mysql
小镇敲码人20 小时前
【深入浅出】之Qt 事件系统实战:鼠标、键盘、滚轮、窗口与定时器事件
数据库·qt·网络协议·mysql·http
Sirens.20 小时前
MySQL数据库:索引
数据库·mysql
2601_9620725020 小时前
大数据-234 离线数仓 - 异构数据源 DataX 将数据 从 HDFS 到 MySQL
大数据·mysql·hdfs