数据库圣经-分析 MySQL 事务隔离级别与并发问题

1.什么是事务?

事务一组SQL语句 打包成为一个整体,在这组SQL的执行此过程中,要么全部成功,要么全部失 败。这组SQL语句可以是一条也可以是多条。来看一个转账的例子,如图:

sql 复制代码
create table account(
id bigint primary key AUTO_INCREMENT,
Bname VARCHAR(10) not null,
balance decimal(10,2) not null
);

insert into account(Bname ,balance) values ("罗峰",1000);
insert into account(Bname ,balance) values ("爱丽丝",1000);

select * from account;

update account set balance = balance - 100 where bname = "罗峰";
update account set balance = balance + 100 where bname = "爱丽丝";

这个是不对的,此时应该是执行了两次转账的记录应该是800,结果却是900,违法了事务的隔离性

  • 如果转账成功,应该有以下结果:
  1. 张三的账户余额减少 100,变成 900,李四的账户余额增加了 100,变成 1100,不能出现张三的余额减少而李四的余额没有增加的情况;
  2. 张三和李四在发生转账前后的总额不变,也就是说转账前张三和李四的余额总数为 1000+1000=2000,转账后他们的余额总数为 900+1100=2000;
  3. 转账后的余额结果应当保存到存储介质中,以便以后读取;
  4. 还有一点需要要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰;

以上这四点在事务的整个执行过程中必须要得到保证,这也就是事务的 ACID 特性

2.事务的 ACID 特性

事务的 ACID 特性 指的是Atomicity (原子性)Consistency (一致性)Isolation (隔离性)Durability (持久性)。

  • Atomicity (原子性):一个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执行了一半的情况,如果事务在执行过程中发生错误,会回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样;
  • Consistency (一致性):在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表示写入的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服务器崩溃后如何恢复;
  • Isolation (隔离性):数据库允许多个并发事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务可以指定不同的隔离级别,以权衡在不同的应用场景下数据库性能和安全;
  • Durability(持久性):事务处理结束后,对数据的修改将永久的写入存储介质,即便系统故障也不会丢失。

3.为什么要使用事务?

事务具备的 ACID 特性,是我们使用事务的原因,在我们日常的业务场景中有大量的需求要用事务来保证。支持事务的数据库能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,在使用事务过程中,要么提交,要么回滚,不用去考虑网络异常,服务器宕机等其他因素,因此我们经常接触的事务本质上是数据库对 ACID 模型的一个实现,是为应用层服务的。

4.如何使用事务

4.1 查看支持事务的存储引擎

  • 要使用事务那么数据库就要支持事务,在 MySQL 中支持事务的存储引擎是 InnoDB,可以通过show engines;语句查看:

4.2 语法

• 通过以下语句可以完成对事务的控制:

sql 复制代码
# 开始⼀个新的事务
START TRANSACTION;
# 或
BEGIN;
#或者
begin transaction;
# 提交当前事务,并对更改持久化保存
COMMIT;
# 回滚当前事务,取消其更改
ROLLBACK;
  • START TRANSACTIONBEGIN 开始一个新的事务;
  • COMMIT 提交当前事务,并对更改持久化保存;
  • ROLLBACK 回滚当前事务,取消其更改;
  • 无论提交还是回滚,事务都会关闭

4.3开启一个事务,执行修改后回滚

sql 复制代码
# 开启事务
START TRANSACTION;

# 在修改之前查看表中的数据
select * from bank_account;

# 张三余额减少100
UPDATE bank_account set balance = balance - 100 where name = '张三';

# 李四余额增加100
UPDATE bank_account set balance = balance + 100 where name = '李四';

# 在修改之后,提交之前查看表中的数据,余额已经被修改
select * from bank_account;

# 回滚事务
ROLLBACK;

# 再查询发现修改没有生效
select * from bank_account;

4.4 开启一个事务,执行修改后提交

sql 复制代码
# 开启事务
BEGIN;

# 在修改之前查看表中的数据
SELECT * FROM bank_account;

# 张三余额减少100
UPDATE bank_account SET balance = balance - 100 WHERE name = '张三';

# 李四余额增加100
UPDATE bank_account SET balance = balance + 100 WHERE name = '李四';

# 在修改之后,提交之前查看表中的数据,余额已经被修改
SELECT * FROM bank_account;

# 提交事务
COMMIT;

# 再查询发现数据已被修改,说明数据已经持久化到磁盘
SELECT * FROM bank_account;

4.5 保存点

在事务执行的过程中设置保存点,回滚时指定保存点可以把数据恢复到保存点的状态

sql 复制代码
# 开启事务
START TRANSACTION;

# 在修改之前查看表中的数据
SELECT * FROM bank_account;

# 张三余额减少100
UPDATE bank_account SET balance = balance - 100 WHERE name = '张三';

# 李四余额增加100
UPDATE bank_account SET balance = balance + 100 WHERE name = '李四';

# 余额已经被修改
SELECT * FROM bank_account;

# 设置第一个保存点
SAVEPOINT savepoint1;

# 再次执行,张三余额减少100
UPDATE bank_account SET balance = balance - 100 WHERE name = '张三';

# 再次执行,李四余额增加100
UPDATE bank_account SET balance = balance + 100 WHERE name = '李四';

# 余额已经被修改
SELECT * FROM bank_account;

# 设置第二个保存点
SAVEPOINT savepoint2;

# 插入一条新记录
INSERT INTO bank_account VALUES (null, '王五', 1000);

# 查询插入后的结果
SELECT * FROM bank_account;

# 回滚到第二个保存点
ROLLBACK TO savepoint2;

# 回滚成功后查询
SELECT * FROM bank_account;

注意:如果是没有写那个rollback;

回滚时不指定保存点,直接回滚到事务开始时的原始状态,事务关闭

sql 复制代码
# 回滚时不指定保存点,直接回滚到事务开始时的原始状态,事务关闭
ROLLBACK;

# 查看原始状态
SELECT * FROM bank_account;

4.6、自动/手动提交事务

默认情况下,MySQL是自动提交事务的,也就是说我们执行的每个修改操作,比如插入、更新和删除,都会自动开启⼀个事务并在语句执行完成之后自动提交,发⽣异常时自动回滚。

查看当前事务是否自动提交可以使用以下语句

sql 复制代码
show variables like 'autocommit';

可以通过以下语句设置事务为自动或手动提交

sql 复制代码
# 设置事务⾃动提交 
SET AUTOCOMMIT=1;     # ⽅式⼀
SET AUTOCOMMIT=ON;    # ⽅式二
# 设置事务⼿动提交 
SET AUTOCOMMIT=0;     # ⽅式⼀
SET AUTOCOMMIT=OFF;   # ⽅式⼆
1.自动提交的默认行为 是 "一条语句一个事务",但通过显式开启事务或关闭autocommit,可以让自动提交模式下的事务包含多条语句
  • 默认情况下,MySQL 事务是自动开启、提交和回滚 的,一个事务仅包含 1 条 DML 语句(如INSERT/UPDATE/DELETE)。

  • 默认自动提交(autocommit=ON :MySQL 默认将每一条 DML 语句(如INSERT/UPDATE)作为一个独立事务,执行后自动提交。此时一个事务确实仅包含 1 条语句。

  • 显式控制事务(覆盖自动提交) :即使autocommit=ON,若用START TRANSACTION/BEGIN显式开启事务,后续的多条 DML 语句会被合并为一个事务 ,直到COMMIT/ROLLBACK才结束。例如:

    sql 复制代码
    START TRANSACTION; -- 显式开启事务(覆盖自动提交)
    UPDATE bank_account SET balance=balance-100 WHERE name='张三'; -- 语句1
    UPDATE bank_account SET balance=balance+100 WHERE name='李四'; -- 语句2
    COMMIT; -- 提交事务,两条语句作为一个事务执行

关闭自动提交(autocommit=OFF :若设置set autocommit=OFF,则执行的所有 DML 语句会默认加入同一个事务 ,直到COMMIT/ROLLBACK才结束。例如:

sql 复制代码
set autocommit=OFF;
UPDATE bank_account SET balance=balance-100 WHERE name='张三'; -- 语句1
UPDATE bank_account SET balance=balance+100 WHERE name='李四'; -- 语句2
COMMIT; -- 提交事务,两条语句作为一个事务执行
2. autocommit系统变量
  • autocommit是 MySQL 的系统变量,用于标识事务是否自动提交,默认值为ON(开启自动提交)。
  • 可通过show variables like 'autocommit';查看其状态。
  • 修改该变量时不区分大小写 ,例如set autocommit = 0;set autocommit = OFF;均可关闭自动提交。
3. 手动提交的规则
  • 关闭autocommit后,执行修改操作(如DELETE)后,必须通过COMMIT提交或ROLLBACK回滚,才能结束事务。
  • 若使用START TRANSACTION/BEGIN显式开启事务,无论autocommit状态如何,都必须通过COMMIT提交才能持久化数据。
  • 已提交的事务无法回滚。
4. 注意事项
  • 手动提交模式下,无需显式开启事务,执行修改操作后直接用COMMIT/ROLLBACK即可。
  • 重启 MySQL 后,autocommit会恢复为默认的自动提交状态;若需永久改为手动提交,需修改 MySQL 配置文件。

6. 事务的隔离性和隔离级别

6.1 什么是隔离性

MySQL 服务可同时被多个客户端访问,每个客户端的 DML 语句以事务为单位;当不同客户端修改同一张表的同一条数据时,可能相互影响。为保证事务执行过程互不干扰,事务之间需相互隔离,这种特性即为隔离性。

6.2 隔离级别

事务的隔离需考虑 "如何实现隔离、隔离程度、兼顾数据安全与性能" 等问题;事务间不同程度的隔离称为隔离级别,不同隔离级别在性能和安全上做了取舍(有的侧重并发、有的侧重安全、有的二者适中)。

MySQL 的 InnoDB 引擎中,事务隔离级别有四种:

  • READ UNCOMMITTED,读未提交
  • READ COMMITTED,读已提交
  • REPEATABLE READ,可重复读(默认)
  • SERIALIZABLE,串行化
  • READ UNCOMMITTED(读未提交) :事务能读取其他事务未提交 的数据,并发性能高,但会出现 "脏读"(读取到无效数据)、不可重复读、幻读问题。
  • READ COMMITTED(读已提交) :事务只能读取其他事务已提交 的数据,解决了 "脏读",但仍存在不可重复读、幻读问题,是很多数据库的默认级别。
  • REPEATABLE READ(可重复读,MySQL 默认) :同一事务内多次读取同一数据,结果一致,解决了 "脏读""不可重复读",但仍可能出现 "幻读"(InnoDB 通过 MVCC 等机制优化了幻读问题)。
  • SERIALIZABLE(串行化):事务串行执行(相当于单线程),完全隔离,解决所有并发问题,但并发性能极低,仅用于数据一致性要求极高的场景。

6.3 查看和设置隔离级别

一、查看隔离级别

事务隔离级别分全局作用域会话作用域,查看方式:

sql 复制代码
# 全局作用域
SELECT @@GLOBAL.transaction_isolation;

# 会话作用域
SELECT @@SESSION.transaction_isolation;

默认隔离级别为REPEATABLE-READ(可重复读)。

二、设置隔离级别

通过GLOBAL|SESSION指定作用域,语法:

sql 复制代码
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode;
  • 隔离级别(level)
    • REPEATABLE READ(可重复读)
    • READ COMMITTED(读已提交)
    • READ UNCOMMITTED(读未提交)
    • SERIALIZABLE(串行化)
  • 访问模式(access_mode)
    • READ WRITE:事务可读写数据
    • READ ONLY:事务只读,不可写
三、设置示例
  1. 全局作用域(后续事务生效,不影响当前事务):
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  1. 会话作用域(当前会话后续事务生效):
sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  1. 仅对下一个事务生效(之后恢复原级别):
sql 复制代码
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
四、其他设置方式
sql 复制代码
# 方式一:直接赋值(空格用"-"代替)
SET GLOBAL transaction_isolation = 'SERIALIZABLE';
SET SESSION transaction_isolation = 'REPEATABLE-READ';

# 方式二:通过变量赋值
SET @@GLOBAL.transaction_isolation='SERIALIZABLE';
SET @@SESSION.transaction_isolation='REPEATABLE-READ';

1. 全局级别的隔离级别设置

sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • 作用域 :对所有新连接的会话生效(已存在的会话不受影响)。
  • 生效时机:当前会话后续的事务会使用该隔离级别,不影响当前正在执行的事务。
  • 特点:修改的是全局默认规则,新连接的会话会自动继承这个隔离级别。

2. 会话级别的隔离级别设置

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • 作用域 :仅对当前会话生效(不影响其他会话)。
  • 生效时机:当前会话后续的所有事务都会使用该隔离级别,不影响当前正在执行的事务。

3. 事务级别的隔离级别设置(不指定作用域)

如果执行 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;(不写GLOBAL/SESSION):

  • 作用域 :仅对下一个事务生效。
  • 生效时机:下一个事务执行时使用该隔离级别,之后的事务会恢复到之前的隔离级别。

核心区别

设置方式 作用域 生效范围
GLOBAL 所有新会话 新连接的会话后续事务
SESSION 当前会话 当前会话的后续所有事务
不指定作用域 下一个事务 仅下一个事务,之后恢复原级别

6.4 不同隔离级别存在的问题

6.4.1 READ UNCOMMITTED - 读未提交与脏读

6.4.1.1 存在问题

READ UNCOMMITTED(读未提交)这个隔离级别下,事务读取数据没有限制,虽然并发效率高,但会出现数据安全问题:比如事务 A 执行了INSERT语句插入数据,但还没执行COMMIT(提交事务),这时候事务 B 就能读到这条 "还没确定保存" 的数据 ;如果之后事务 A 执行了回滚操作 (把刚才插入的数据撤销了),那事务 B 之前读到的这条数据就成了 "无效数据"------ 相当于读了一个 "不存在的、临时的脏数据",这种现象就叫 "脏读"。

6.4.1.2 问题重现
  1. 客户端 A 设置全局隔离级别为 READ UNCOMMITTED
sql 复制代码
# 设置隔离级别为READ UNCOMMITTED读未提交
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

# 查看设置是否生效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED               | # 已生效
+--------------------------------+
1 row in set (0.00 sec)
  1. 客户端 B 确认隔离级别
sql 复制代码
# 查看设置是否生效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED               | # 已生效
+--------------------------------+
1 row in set (0.00 sec)
  1. 在不同客户端中执行事务

由于 READ UNCOMMITTED 读未提交会出现 "脏读" 现象,在正常的业务中出现这种问题会产生非常危重后果,所以正常情况下应该避免使用 READ UNCOMMITTED 读未提交这种的隔离级别。

6.4.2 READ COMMITTED

  • 读已提交与不可重复读
6.4.2.1 存在问题

为了解决脏读问题,可以把事务的隔离级别设置为READ COMMITTED ,这时事务只能读到了其他事务提交之后的数据,但会出现不可重复读的问题,比如事务A 先对某条数据进行了查询 ,之后事务 B 对这条数据进行了修改 ,并且提交(COMMIT)事务 ,事务 A 再对这条数据进行查询时,得到了事务 B 修改之后的结果,这导致了事务 A 在同一个事务中以相同的条件查询得到了不同的值,这个现象要 "不可重复读"。

6.4.2.2 问题重现
  • 在一个客户端 A 中先设置全局事务隔离级别为 READ COMMITTED 读未提交:
sql 复制代码
 # 设置隔离级别为READ COMMITTED读未提交
 mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
 Query OK, 0 rows affected (0.00 sec)

# 查看设置是否生效
 mysql> SELECT @@GLOBAL.transaction_isolation;
 +--------------------------------+
 | @@GLOBAL.transaction_isolation |
 +--------------------------------+
 | READ-COMMITTED                 | 
# 已生效
 +--------------------------------+
 1 row in set (0.00 sec)
  • 打开另一个客户端 B 并确认隔离级别
sql 复制代码
1 # 查看设置是否生效
2 mysql> SELECT @@GLOBAL.transaction_isolation;
3 +--------------------------------+
4 | @@GLOBAL.transaction_isolation |
5 +--------------------------------+
6 | READ-COMMITTED                 | # 已生效
7 +--------------------------------+
8 1 row in set (0.00 sec)
9

不同的客户端中执⾏事务

6.4.3 REPEATABLE READ - 可重复读与幻读

6.4.3.1 存在问题

为了解决不可重复读问题,可将事务隔离级别设置为 REPEATABLE READ,此时同一个事务中读取的数据在任何时候都是相同的结果,但仍会出现 "幻读" 问题:事务 A 查询一个区间的记录得到结果集 A,事务 B 向这个区间的间隙中写入一条记录并提交,事务 A 再次查询该区间时,会查到事务 B 新写入的记录得到结果集 B,两次查询的结果集不一致,这种现象即为 "幻读"。

MySQL 的 InnoDB 存储引擎使用了 Next-Key 锁解决了大部分幻读问题。

6.4.3.2 问题重现

由于 REPEATABLE READ 隔离级别默认使用了 Next-Key 锁,为了重现幻读问题,需将隔离级回退到更新时只加了排他锁的 READ COMMITTED:

sql 复制代码
1 # 设置隔离级别为READ COMMITTED读未提交
2 mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
3 Query OK, 0 rows affected (0.00 sec)
4
5 # 查看设置是否生效
6 mysql> SELECT @@GLOBAL.transaction_isolation;
7 +--------------------------------+
8 | @@GLOBAL.transaction_isolation |
9 +--------------------------------+
10 | READ-COMMITTED                 | # 已生效
11 +--------------------------------+
12 1 row in set (0.00 sec)
  • 在不同的客户端中执行事务

把隔离级别设置为REPEATABLE-READ后,在ID的间隙中插⼊新数据观察现象,⽐如插⼊ID=4的记 录

区别这三个

1. 不可重复读
  • 定义 :同一事务内,对同一条数据多次查询,结果不一致(其他事务修改并提交了该数据)。
  • 核心特征单条数据的内容被修改,导致同一事务内重复读的结果不同。
  • 场景示例:事务 A 查询 "王五" 的余额为 2000 → 事务 B 修改 "王五" 的余额为 1000 并提交 → 事务 A 再次查询 "王五" 的余额,结果变为 1000。
2. 可重复读
  • 定义 :这是一种隔离级别(MySQL 默认),而非问题。在该级别下,同一事务内对同一条数据的多次查询结果始终一致,解决了 "不可重复读" 问题。
  • 核心作用:保证事务内读取的单条数据的一致性,但无法完全避免 "幻读"。
3. 幻读
  • 定义 :同一事务内,对同一范围数据 多次查询,结果集的条数 / 内容不一致(其他事务在该范围插入 / 删除了新数据)。
  • 核心特征范围数据的条数变化(插入 / 删除导致),区别于 "不可重复读" 的单条数据修改。
  • 场景示例:事务 A 查询 "余额> 1000" 的记录(结果有 2 条)→ 事务 B 插入一条 "余额 2000" 的新记录并提交 → 事务 A 再次查询 "余额 > 1000" 的记录,结果变为 3 条。

6.4.4 SERIALIZABLE

  • 串行化进一步提升事务的隔离级别到 SERIALIZABLE,此时所有事务串行执行,可以解决所有并发中的安全问题。

6.5 不同隔离级别的性能与安全

• InnoDB存储引擎事务隔离性以及相关的隔离级别是由锁和MVCC机制配合实现的,关于锁与 MVCC的相关内容和原⼦性,持久性,⼀致性的相关实现原理可以参阅MySQL进阶中事务章节

相关推荐
xu_yule2 小时前
算法基础(区间DP)
数据结构·c++·算法·动态规划·区间dp
yyy(十一月限定版)3 小时前
c语言——二叉树
c语言·开发语言·数据结构
暗之星瞳3 小时前
mysql表的链接
大数据·数据库·mysql
落羽的落羽3 小时前
【C++】深入浅出“图”——图的基本概念与存储结构
服务器·开发语言·数据结构·c++·人工智能·机器学习·图搜索算法
IT方大同3 小时前
循环结构的功能
c语言·数据结构·算法
@老蝴3 小时前
MySQL - 索引
数据库·mysql
tgethe3 小时前
MySQL 进阶攻略
数据库·mysql
亮子AI3 小时前
【node.js MySQL】node.js 如何连接 MySQL?
数据库·mysql·node.js
sin_hielo3 小时前
leetcode 955
数据结构·算法·leetcode