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

相关推荐
jason_renyu1 小时前
MySQL横表(直表/宽表)与竖表(键值表)完整实操学习笔记
mysql·mysql学习·横标和竖表·竖表讲解·横标竖表学习
梦想的颜色2 小时前
MySQL 查询性能核武器
运维·服务器·数据结构·数据库·mysql
haven-8522 小时前
mysql索引当中的B+树,聚簇/二级索引,最左匹配,失效场景
数据库·b树·mysql
jason_renyu3 小时前
MySQL数据表设计入门学习文档(基于Flask+Vue3图书馆管理系统·小白专用)
mysql·数据表设计入门学习·mysql数据库表设计学习·新手入门数据表设计
cui_ruicheng4 小时前
MySQL(一):数据库基础与MySQL入门
数据库·sql·mysql
Database_Cool_4 小时前
AnalyticDB MySQL vs ClickHouse:OLAP 数据库选型深度对比——谁更适合企业级分析
数据库·数据仓库·mysql·数据分析
AOwhisky4 小时前
MySQL 学习笔记(第三期):SQL 语言之数据操作与单表查询
linux·运维·笔记·sql·学习·mysql·云计算
川石课堂软件测试7 小时前
性能测试|JMeter常用线程组设置策略
大数据·数据库·功能测试·测试工具·jmeter·mysql·单元测试
Database_Cool_7 小时前
企业级多模态分析计算引擎选型:阿里云 AnalyticDB MySQL 统一分析平台方案
数据库·mysql·阿里云