首页  /  数据库  /  正文

MySQL 事务与隔离级别

数据库 2026-10-02📖 16 分钟👁 —
🐬 数据库 · MySQL第 4 / 12 页123456789101112📖 完整导航 →

事务是一组操作的集合,要么全部成功,要么全部失败。

流程是:开启事务 → 执行 SQL →(出异常就)回滚事务 → 提交事务。

单条 SQL 其实是 MySQL 自动帮我们包了一个事务,所以我们平时感觉不到它。

一、操作演示

相当于给执行 SQL 多加了一个确认(commit)的步骤,回滚就是取消(出错的时候用)。

1. 准备数据

create table account(
    id int auto_increment primary key comment '主键ID',
    name varchar(10) comment '姓名',
    money int comment '余额'
) comment '账户表';

insert into account(id, name, money) values
(null, '张三', 2000),
(null, '李四', 2000);

# 恢复数据
update account set money = 2000 
where name = '张三' or name = '李四';

# 把事务提交改为手动
# 1. 查询张三账户余额
select * from account where name = '张三';

# 2. 将张三账户余额-1000
update account set money = money - 1000 where name = '张三';

# 3. 将李四账户余额+1000
update account set money = money + 1000 where name = '李四';

# 4. 执行完 sql 手动提交事务才会执行 sql
commit;

# 如果部分执行出错,所有选择的 sql 都失效,需要回滚事务

2. 改变事务提交方式

把自动提交关掉,之后的 SQL 就得手动 commit 才生效:

# 查看当前事务提交方式(1为自动提交,0为手动提交)
select @@autocommit;

# 设置事务提交方式为手动
set @@autocommit = 0;

# 提交事务(确认生效)
commit;

# 回滚事务(撤销操作)
rollback;

3. 不改变事务的提交方式

不想动全局设置的话,就在这一批 SQL 前面加一条 start transaction:

# 开启事务:在选中所有的 sql 之前加一条
# transaction 译为交易
start transaction;
# 或
begin;

# 提交事务
commit;

# 回滚事务
rollback;

# 全部选中执行
start transaction;
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
commit;

4. 四大特性 ACID

事务四大特性:原子性、一致性、隔离性、持久性

二、事务并发问题

先区分一下:这里的并发不是并行。

两个事务同时跑,就可能踩出下面三种问题:

问题描述
脏读一个事务读到另外一个事务还没有提交的数据
不可重复读一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读
幻读一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了"幻影"

1. 脏读

脏读只有特定隔离级别才会发生,一般来说不会发生,也就是只有另外的事务提交了数据才能读到。

# 事务A
start transaction;
update account set money = money - 1000 where name = '张三';  # 扣了1000,但还没提交

# 事务B(此时读取,A没提交就读到了)
start transaction;
select * from account where name = '张三';   # 读到 money = 1000(事务A还没提交的数据)

# 事务A 反悔了
rollback;   # 钱恢复成2000

# 事务B 拿着刚才读到的 1000 去干别的事...
# 实际上数据库里还是 2000,事务B读到的数据是"脏"的

事务 A 改了 id=1 但还没提交,事务 B 就把这个中间值读走了:

脏读:事务A 改了 id=1 还没提交,事务B 就读到了

2. 不可重复读

事务 A 先查了一次,然后事务 B 把数据改了,事务 A 再查一次,两次结果不一样,这就叫不可重复读。

# 事务A
start transaction;

# 第一次查询,查张三余额
select money from account where name = '张三';   # 返回 2000

# 事务B(在A第一次查询之后执行)
start transaction;

# 修改张三余额并提交
update account set money = 3000 where name = '张三';
commit;

# 事务A 再次查询同一行
select money from account where name = '张三';   # 返回 3000

# 两次结果不同 → 不可重复读
# 如果隔离级别不允许不可重复读,那么A就会读到2000,即可重复读
# A 提交之后再读,才是 3000
commit;

# 这么看,不可重复读似乎是好事,因为它可以在事务未提交时看到数据变化
# 这样可以防止一些决策错误,但是MySQL默认关闭了它

# 因为会导致别的问题:
# 基于第一次读到的数据做的决策,到第二次读的时候已经失效了
# 程序里用的"判断值"和数据库里的"当前值"不一致
# 不可重复读的伤害是"阴"的:程序不报错,但业务逻辑跑偏了

# 而且会用别的方式保证数据一致性:锁

可重复读的意思是:同一个事务里读同一行两次,拿到的结果是一样的。上面这个例子里两次读到 2000 和 3000,就是没做到可重复读。

不可重复读:事务A 前后两次查 id=1,中间被事务B 改了并提交

3. 幻读

幻读往往跟主键有关,因为主键值必须唯一,和其他的字段不一样。

幻读的核心:第一次查的时候,那行数据确实不存在,但想操作它的时候,它突然冒出来了,像幻觉一样。

# 事务A
start transaction;

# 第一次查:没有 id=5 的用户
select * from user where id = 5;   # 结果:空

# 事务B 插入了一条 id=5 的数据,并且提交了
start transaction;
insert into user(id, name) values (5, '张三');
commit;

# 事务A 想插入 id=5 的用户
insert into user(id, name) values (5, '李四');   
报错:主键冲突

# 此时 事务A 再次查询数据,发现没有:
# 因为事务A的"快照"在第一次查询时就固定了
# select 读的是快照,insert 是去真实表里检查的

# 事务A 根据快照:id=5 不存在,可以插
# 真实数据库:id=5 已经被事务B插进去了
# 执行 insert 时,数据库去真实表里检查 → 发现冲突 → 报错

快照里没有但是真实的表有,这就是矛盾点。

看着好像开一下不可重复读能解决幻读 —— 在执行改变数据的操作前再查一次就行了 —— 但这显然不合理,也不符合业务逻辑,所以 MySQL 默认是关掉它的。

幻读:事务A 两次查 id=1 都是空,插入时却和事务B 插的数据撞了主键

三、事务隔离级别

隔离级别越高,上面这些问题就越少:

隔离级别脏读不可重复读幻读
read uncommitted(低级别)√√√
read committed(Oracle 默认)×√√
repeatable read(MySQL 默认)××√
serializable×××

隔离级别低,安全性差,但是性能高。

# 查看事务隔离级别
# isolation 译为隔离
select @@transaction_isolation;

# 设置事务隔离级别
set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable}

session 和 global 的区别:

sessionglobal
作用范围当前这次连接(客户端窗口)整个MySQL服务器(永久的)
影响谁只影响自己影响所有新连接
重启后失效失效(重启恢复成配置文件里的值)
常用场景临时调一下,自己看看效果DBA统一设置,影响所有人

调一下看看效果:

# 设置隔离级别
set session transaction isolation level read uncommitted;
select @@transaction_isolation;

事务执行是写入了库中的,并不是模拟执行,所以才有了回滚的操作。

如果是模拟执行,那直接改 SQL 就行了,不需要回滚。

start transaction;

# 1. 执行更新
update account set money = money - 1000 where name = '张三';

# 2. 此时数据已经在数据库里改了(但不是最终状态)
# 3. 其他事务如果隔离级别够低,甚至能看到修改后的值

# 4. 决定提交或回滚
commit;    # 把改动正式写入,永久生效
rollback;  # 把改动撤销,回到事务开始前的状态

关闭幻读靠的是最高级别 serializable,做法是在所有读操作上加锁:

# 幻读是A执行时,B改了数据
# 关闭幻读:A执行时B就改不了数据
# 让事务A执行期间,B无法插入、修改、删除任何A可能涉及的数据
# B的事务会被阻塞,直到A提交或回滚

四、收个尾

事务这块的要点放一张图里:

事务小结:事务操作、四大特性、并发问题、四种隔离级别

至此,MySQL 基础部分就结束了。