跳到主要内容
MySQL 事务:从基础概念到隔离性理论与实践 | 极客日志
SQL
MySQL 事务:从基础概念到隔离性理论与实践 MySQL 事务是保证数据一致性的核心机制,具备 ACID 特性。涵盖事务的原子性、一致性、隔离性与持久性,对比 InnoDB 与 MyISAM 引擎支持差异。通过实操演示自动提交与手动提交模式,解析保存点回滚及异常终止场景。重点阐述四种隔离级别及其引发的脏读、不可重复读与幻读问题,提供查看与设置隔离级别的命令方法,帮助开发者理解并发控制原理。
PhpPioneer 发布于 2026/3/30 更新于 2026/4/25 1 浏览MySQL 事务:从基础概念到隔离性理论与实践
本文详细介绍了 MySQL 事务的概念与特性。事务是由一组 DML 语句组成的逻辑单元,具有 ACID 特性:原子性 (全部成功或失败)、一致性 (数据完整性)、隔离性 (并发控制) 和持久性 (永久修改)。重点分析了事务的隔离级别:读未提交 (可能脏读)、读提交 (可能不可重复读),后续章节将深入探讨可重复读与串行化。
什么是事务?
事务是由一组 DML 语句组成,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。MySQL 提供一种机制,保证我们达到这样的效果。事务就是要做的或所做的事情,主要用于处理操作量大、复杂度高的数据。
一个 MySQL 数据库,不止一个事务在运行,同一时刻甚至有大量的请求被包装成事务向 MySQL 服务器发起事务处理请求。每条事务至少包含一条 SQL,也可能有很多条。如果大家都访问同样的表数据,在不加保护的情况下,绝对会出现问题。特别是事务由多条 SQL 构成时,执行到一半出错或者不想再执行的情况时有发生,那么已经执行的怎么办呢?
所以,一个完整的事务,绝对不是简单的 SQL 集合,还需要满足如下四个属性:
原子性 :一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚 (Rollback) 到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性 :在事务开始之前和事务结束以后,数据库的完整性没有被破坏。也就是说写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性 :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交 (Read Uncommitted)、读提交 (Read Committed)、可重复读 (Repeatable Read) 和串行化 (Serializable)。
持久性 :事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
只要我们做到了事务的原子性、隔离性、持久性,就能在技术上保证数据的一致性。
上面四个属性,简称为 ACID:
原子性 (Atomicity)
一致性 (Consistency)
隔离性 (Isolation)
持久性 (Durability)
为什么存在事务?
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了、服务器宕机了、同时更改一个数据怎么办对吧?
因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
版本支持
在 MySQL 中只有使用了 InnoDB 数据库存储引擎的数据库或表才支持事务,MyISAM 不支持。
查看数据库引擎
mysql> show engines;
mysql> show engines \G
输出示例中可以看到 InnoDB 的 Transactions: YES,而 MyISAM 为 NO。
事务提交方式
事务的提交方式常见的有两种:自动提交和手动提交。
查看事务提交方式
show variables like 'autocommit' ;
设置自动提交模式 set autocommit= 0 ;
set autocommit= 1 ;
常见操作方式 为了便于演示,我们将 MySQL 的默认隔离级别设置成读未提交。具体操作我们后面专门会讲,现在以使用为主。
mysql> set global transaction isolation level READ UNCOMMITTED;
mysql> quit Bye
mysql> select @@tx_isolation ;
通过开启两个 MySQL 的客户端来充当两个并发访问 MySQL 服务的客户端,用这两个客户端来制造并发场景,来研究事务并发运行的情况。
创建测试表 create table if not exists account(
id int primary key ,
name varchar (50 ) not null default '' ,
balance decimal (10 ,2 ) not null default 0.0
) ENGINE= InnoDB DEFAULT CHARSET= UTF8;
正常演示 - 证明事务的开始与回滚 mysql> show variables like 'autocommit' ;
+
| Variable_name | Value |
+
| autocommit | ON |
+
1 row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> savepoint save1;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1 , '张三' , 100 );
Query OK, 1 row affected (0.05 sec)
mysql> savepoint save2;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into account values (2 , '李四' , 10000 );
Query OK, 1 row affected (0.00 sec)
mysql> select * from account;
+
| id | name | balance |
+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+
2 rows in set (0.00 sec)
mysql> rollback to save2;
Query OK, 0 rows affected (0.03 sec)
mysql> select * from account;
+
| id | name | balance|
+
| 1 | 张三 | 100.00 |
+
1 row in set (0.00 sec)
mysql> rollback ;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
Empty set (0.00 sec)
也可以不用设置保存点,开始事务开始之后直接 rollback,所有的数据直接清空;回滚只能在事务运行期间操作,一旦事务提交无法回滚。
非正常演示 1 - 未 commit,客户端崩溃,MySQL 自动会回滚(隔离级别设置为读未提交) -- 终端 A
mysql> select * from account; -- 当前表内无数据
Empty set (0.00 sec)
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; -- 数据已经存在,但没有 commit,此时同时查看 终端 B
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> Aborted -- ctrl + \ 异常终止 MySQL
-- 终端 B
mysql> select * from account; -- 终端 A 崩溃前
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> select * from account; -- 数据自动回滚
Empty set (0.00 sec)
非正常演示 2 - 证明 commit 了,客户端崩溃,MySQL 数据不会再受影响,已经持久化 -- 终端 A
mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
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.04 sec)
mysql> Aborted -- ctrl + \ 异常终止 MySQL
-- 终端 B
mysql> select * from account; -- 数据存在了,所以 commit 的作用是将数据持久化到 MySQL 中
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
非正常演示 3 - 对比试验。证明 begin 操作会自动更改提交方式,不会受 MySQL 是否自动提交影响 -- 终端 A
mysql> select *from account; -- 查看历史数据
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 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; -- 查看插入记录,同时查看终端 B
+----+--------+----------+
| id | name | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> Aborted -- 再次异常终止
-- 终端 B
mysql> select * from account; -- 终端 A 崩溃前
+----+--------+----------+
| id | name | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; -- 终端 A 崩溃后,自动回滚
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
非正常演示 4 - 证明单条 SQL 与事务的关系 -- 终端 A
mysql> select * from account;
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 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> insert into account values (2, '李四', 10000); -- 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> select *from account; -- 查看结果,已经插入。此时可以在查看终端 B
+----+--------+----------+
| id | name | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> ^DBye -- ctrl + \ or ctrl + d, 终止终端
-- 终端 B
mysql> select * from account; -- 终端 A 崩溃前
+----+--------+----------+
| id | name | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; -- 终端 A 崩溃后
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
-- 终端 A
mysql> show variables like 'autocommit'; -- 开启默认提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select * from account;
+----+--------+--------+
| id | name | balance|
+----+--------+--------+
| 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 | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> Aborted -- 异常终止
-- 终端 B
mysql> select * from account; -- 终端 A 崩溃前
+----+--------+----------+
| id | name | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; -- 终端 A 崩溃后,并不影响,已经持久化。autocommit 起作用
+----+--------+----------+
| id | name | balance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
只要输入 begin 或者 start transaction,事务便必须要通过 commit 提交,才会持久化,与是否设置 set autocommit 无关。
事务可以手动回滚,同时,当操作异常,MySQL 会自动回滚。
对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select 有特殊情况,因为 MySQL 有 MVCC)。
从上面的例子,我们能看到事务本身的原子性 (回滚),持久性 (commit)。
那么隔离性?一致性?
如果没有设置保存点,也可以回滚,只能回滚到事务的开始。
直接使用 rollback(前提是事务还没有提交)。如果一个事务被提交了 (commit),则不可以回退 (rollback)。
可以选择回退到哪个保存点。InnoDB 支持事务,MyISAM 不支持事务。
开始事务可以使用 start transaction 或者 begin。
事务的隔离性理论
如何理解隔离性
MySQL 服务可能会同时被多个客户端进程 (线程) 访问,访问的方式以事务方式进行。
一个事务可能由多条 SQL 构成,也就意味着,任何一个事务,都有执行前、执行中、执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个 SQL 的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。
数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
隔离级别 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交 commit 的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
读提交【Read Committed】: 该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select,可能得到不同的结果。
可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。
隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁 (GAP), Next-Key 锁 (GAP+ 行锁) 等。不过,我们目前现有这个认识就行,先关注上层使用。
查看与设置隔离性 select @@global .tx_isolation;
select @@session .tx_isolation;
select @@tx_isolation ;
SET [SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
仅修改当前 session 隔离级别并不会影响全局事务隔离级别,只影响当前会话,当全局和当前隔离级别不同时,会就近原则使用 session 隔离级别。
当修改全局 global 隔离级别时,当前 session 隔离级别并不会立刻跟随全局隔离级别进行同步修改,只有在重新登录时 session 隔离级别会默认是 global 隔离级别。
读未提交【Read Uncommitted】 几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用。
一个事务在执行中,读到另一个执行中事务的更新 (或其他操作) 但是未 commit 的数据,这种现象叫做脏读 (dirty read)。
读提交【Read Committed】 此时还在当前事务中,并未 commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段 (依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读 (non reapeatable read)!!(这个是问题吗??是!)我们不希望这种现象的出现。
至此事务部分讲解完毕,后续将继续深入讨论更多高级特性。
相关免费在线工具 SQL 美化和格式化 在线格式化和美化您的 SQL 查询(它支持各种 SQL 方言)。 在线工具,SQL 美化和格式化在线工具,online
SQL转CSV/JSON/XML 解析 INSERT 等受限 SQL,导出为 CSV、JSON、XML、YAML、HTML 表格(见页内语法说明)。 在线工具,SQL转CSV/JSON/XML在线工具,online
CSV 工具包 CSV 与 JSON/XML/HTML/TSV/SQL 等互转,单页多 Tab。 在线工具,CSV 工具包在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online