事务是一组操作的集合,要么全部成功,要么全部失败。
流程是:开启事务 → 执行 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

- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
二、事务并发问题
先区分一下:这里的并发不是并行。
两个事务同时跑,就可能踩出下面三种问题:
| 问题 | 描述 |
|---|---|
| 脏读 | 一个事务读到另外一个事务还没有提交的数据 |
| 不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读 |
| 幻读 | 一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了"幻影" |
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 就把这个中间值读走了:

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,就是没做到可重复读。

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 默认是关掉它的。

三、事务隔离级别
隔离级别越高,上面这些问题就越少:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 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 的区别:
session | global | |
|---|---|---|
| 作用范围 | 当前这次连接(客户端窗口) | 整个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 基础部分就结束了。