事务到底是什么?为什么数据库需要事务?
1. 为什么需要事务?
假设现在有一个转账业务。
用户 A 给用户 B 转:
text
100 元
业务逻辑:
text
A 账户 -100
↓
B 账户 +100
对应 SQL:
sql
UPDATE account
SET balance = balance - 100
WHERE id = 1;
UPDATE account
SET balance = balance + 100
WHERE id = 2;
正常情况下:
text
A:1000 → 900
B:500 → 600
没有问题。
但是假设第一条 SQL 执行成功:
text
A:1000 → 900
然后系统突然:
text
宕机
第二条 SQL 没有执行。
最终:
text
A 少了 100
B 没收到 100
钱凭空消失了。
这显然是不允许的。
因此我们希望:
这两条 SQL 要么全部成功,要么全部失败。
这就是事务最核心的作用。
2. 什么是事务?
事务可以简单理解成:
把多个数据库操作作为一个整体执行。
例如:
text
事务开始
↓
A -100
↓
B +100
↓
事务提交
如果中途失败:
text
事务开始
↓
A -100
↓
发生异常
↓
回滚
数据库会把之前的操作撤销。
于是:
text
A:仍然是 1000
B:仍然是 500
所以事务可以简单记成:
text
全部成功
或者
全部失败
3. MySQL 中怎么使用事务?
最基本的写法:
sql
START TRANSACTION;
UPDATE account
SET balance = balance - 100
WHERE id = 1;
UPDATE account
SET balance = balance + 100
WHERE id = 2;
COMMIT;
其中:
text
START TRANSACTION
表示:
text
开启事务
然后执行多条 SQL。
最后:
sql
COMMIT;
表示:
text
提交事务
只有提交以后,这个事务才正式完成。
如果中间出现问题:
sql
ROLLBACK;
就可以:
text
回滚事务
也就是撤销本次事务中的修改。
4. 事务最经典的四个特性:ACID
数据库事务通常需要满足四个特性:
text
A
Atomicity 原子性
C
Consistency 一致性
I
Isolation 隔离性
D
Durability 持久性
简称:
text
ACID
5. Atomicity:原子性
一个事务中的操作,要么全部执行成功,要么全部失败。
例如:
text
A -100
B +100
不能出现:
text
A -100 成功
B +100 失败
而是:
text
情况一:
A -100
B +100
↓
一起提交
或者:
text
情况二:
A -100
B +100 失败
↓
全部回滚
所以原子性可以简单理解成:
事务是一个不可再拆分的整体。
6. Consistency:一致性
转账之前:
text
A = 1000
B = 500
总金额 = 1500
转账以后:
text
A = 900
B = 600
总金额 = 1500
虽然两个账户的数据变了:
text
1000 → 900
500 → 600
但是:
text
总金额
没有变化。
这说明数据库从一个:
text
合法状态
转换到了另一个:
text
合法状态
这就是一致性。
可以简单理解成:
事务执行前后,数据库都应该满足业务规则和数据约束。
例如:
text
余额不能小于 0
订单金额不能为负数
用户名必须唯一
这些都属于一致性的一部分。
7. 一致性是不是完全靠 MySQL 保证?
不是。
数据库可以帮我们保证:
text
主键约束
唯一约束
外键约束
事务机制
但是业务一致性很多时候还需要:
text
应用代码
例如:
text
账户余额不能为负数
可能需要业务自己判断:
sql
UPDATE account
SET balance = balance - 100
WHERE id = 1
AND balance >= 100;
所以:
一致性其实是事务最终想达到的目标,而原子性、隔离性、持久性等机制共同帮助实现一致性。
8. Isolation:隔离性
这个概念和:并发事务有关。
假设现在:
text
事务 A
正在修改一个账户:
text
balance = 1000
然后事务 A:
text
减去 100
变成:
text
balance = 900
但是:
text
事务 A 还没有提交
这时候:
text
事务 B
也来查询余额。
问题来了:
事务 B 应不应该看到 900?
如果看到:
text
900
结果事务 A 最后:
text
ROLLBACK
那么余额又变回:
text
1000
事务 B 刚才看到的:
text
900
其实是一个:
text
从未真正提交的数据
这就会产生并发问题。
所以事务之间需要一定程度的:
text
隔离
这就是:
一个事务的执行不应该随意受到其他并发事务的影响。
9. Durability:持久性
一个事务一旦提交成功,它的结果就应该被永久保存。
例如:
text
COMMIT
已经返回成功。
那么即使之后:
text
MySQL 崩溃
服务器重启
系统断电
已经提交的数据也应该能够恢复。
例如:
text
A = 900
B = 600
不能重启以后:
text
A 又变回 1000
B 又变回 500
在 InnoDB 中,持久性和:
text
redo log
有非常重要的关系。
10. ACID 怎么一起理解?
还是转账:
text
A -100
B +100
原子性
text
两个操作要么全部成功
要么全部失败
一致性
text
转账前后数据都满足业务规则
隔离性
text
并发事务之间不能随意互相干扰
持久性
text
事务提交成功后
结果不能因为系统崩溃而消失
11. 为什么并发事务会产生问题?
数据库不可能:
text
一次只允许一个用户操作
真实系统中通常会有:
text
事务 A
事务 B
事务 C
事务 D
...
同时执行。
并发能够提高系统性能。
但同时也会产生一些问题。
最经典的三个:
text
脏读
不可重复读
幻读
理解事务隔离级别,首先就得理解它们。
12. 什么是脏读?
假设:
text
事务 A
执行:
sql
UPDATE account
SET balance = 900
WHERE id = 1;
但是:
text
还没有 COMMIT
这时候事务 B:
sql
SELECT balance
FROM account
WHERE id = 1;
如果读到了:
text
900
那么:
text
事务 B
读取到了:
text
事务 A 尚未提交的数据
接着事务 A:
sql
ROLLBACK;
余额又恢复:
text
1000
那么事务 B 刚才读到的:
text
900
就是一个无效数据。
text
Dirty Read 脏读
读到了别人还没提交的数据。
13. 什么是不可重复读?
假设事务 A:
sql
SELECT balance
FROM account
WHERE id = 1;
第一次查询:
text
1000
然后事务 B:
sql
UPDATE account
SET balance = 900
WHERE id = 1;
COMMIT;
事务 B 修改并提交了。
接着事务 A:
sql
SELECT balance
FROM account
WHERE id = 1;
第二次查询:
text
900
于是:
text
同一个事务
第一次读:1000
第二次读:900
两次结果不一样。
这就是:
text
Non-Repeatable Read 不可重复读
同一个事务里,同一行数据读两次,值变了。
14. 什么是幻读?
假设事务 A:
sql
SELECT *
FROM user
WHERE age > 18;
第一次查出来:
text
10 条
然后事务 B 插入:
sql
INSERT INTO user(...)
VALUES (...);
这条新数据:
text
age = 20
然后事务 B:
sql
COMMIT;
事务 A 再次执行:
sql
SELECT *
FROM user
WHERE age > 18;
结果:
text
11 条
突然多出了一条。
好像出现了幻影,所以叫:
text
Phantom Read 幻读
同一个范围查询执行两次,结果集中的行数发生变化。
15. 不可重复读和幻读有什么区别?
不可重复读
重点是:
text
某一行的数据变了
例如:
text
第一次:
id = 1
balance = 1000
第二次:
id = 1
balance = 900
还是同一行,只是值变了。
幻读
重点是:
text
结果集中出现了新行
或者少了某些行
例如:
text
第一次:10 条
第二次:11 条
16. 怎么解决这些并发问题?
最简单粗暴的方法:
text
所有事务一个一个执行
text
事务 A
↓
执行完
事务 B
↓
执行完
事务 C
这样肯定不会产生并发问题,但是性能太差。
所以数据库需要在并发性能和数据隔离之间做平衡。
于是就出现了:
text
事务隔离级别
17. SQL 标准四种隔离级别
SQL 标准定义了四种经典事务隔离级别:
text
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
隔离程度:
text
低
↓
READ UNCOMMITTED
↓
READ COMMITTED
↓
REPEATABLE READ
↓
SERIALIZABLE
↓
高
通常:
text
隔离性越高
意味着:
text
并发限制越强
因此一般来说:
text
性能和并发能力可能越低
18. READ UNCOMMITTED
text
读未提交
一个事务可以读取另一个事务尚未提交的数据。
例如:
text
事务 A:
UPDATE balance = 900
还没 COMMIT
事务 B:
text
已经可以看到 900
因此它可能产生:
text
脏读
同时:
text
不可重复读
幻读
也都可能发生。
所以:
text
READ UNCOMMITTED
隔离级别非常低。
实际业务中通常很少使用。
19. READ COMMITTED
text
读已提交
只能读取其他事务已经提交的数据。
例如:
text
事务 A
修改 balance = 900
但没有提交
事务 B:
text
看不到 900
等事务 A:
text
COMMIT
以后,事务 B 才能看到。
脏读被解决了。
但是,不可重复读 和 幻读仍然可能出现。
20. READ COMMITTED 为什么会不可重复读?
假设事务 A:
text
第一次查询:
balance = 1000
事务 B:
text
修改为 900
↓
COMMIT
事务 A 再查:
text
balance = 900
因为:
text
事务 B 已经提交
所以 READ COMMITTED 允许事务 A 看到最新已提交数据。
于是:
text
第一次 1000
第二次 900
产生:
text
不可重复读
所以可以简单理解:
text
READ COMMITTED
↓
每次读取当前已经提交的最新版本
21. REPEATABLE READ
text
可重复读
这是 InnoDB 中非常重要的隔离级别,也是 MySQL InnoDB 默认使用的事务隔离级别。
它希望做到:
同一个事务中,对同一数据进行多次一致性读取,看到的结果保持一致。
例如事务 A:
text
第一次:
balance = 1000
然后事务 B:
text
修改:
balance = 900
并 COMMIT
事务 A 再查询:
text
1000
所以:
text
第一次:1000
第二次:1000
实现:
text
Repeatable Read 可重复读
22. MySQL 是不是把旧数据复制了一份?
看到这里就会产生一个问题:
事务 B 明明已经update并且commit了。
为什么事务 A 还能看到:
text
旧版本 1000
这就是接下来非常重要的:
text
MVCC
Multi-Version Concurrency Control
多版本并发控制。
可以先简单理解成:
text
同一行数据
↓
可能存在多个历史版本
不同事务根据:
text
自己的可见性规则
决定:
text
应该看到哪个版本
所以事务 A 和事务 B:
text
同时查询同一条数据
可能看到不同版本。
23. REPEATABLE READ 能不能解决幻读?
如果按照 SQL 标准的经典定义理解,它主要解决:
text
脏读
不可重复读
幻读仍然是更高隔离级别需要处理的问题。
但是 InnoDB 的实际实现更加复杂。
InnoDB 在:
text
快照读
中通过:
text
MVCC
可以让同一个事务中的一致性读取维持一个稳定的快照,因此很多普通 SELECT 场景不会看到新插入的数据。
而对于:
text
当前读
又会结合:
text
Next-Key Lock
等锁机制控制范围内的并发修改。
所以不能简单说:
text
MySQL RR 一定会出现幻读
或者:
text
MySQL RR 完全不存在任何幻读问题
更加准确地说:
InnoDB 的 REPEATABLE READ 通过 MVCC 和锁机制,在大量实际场景中对幻读进行了处理。
之后再具体讲锁机制和MVCC
24. SERIALIZABLE
text
SERIALIZABLE 串行化
尽可能让并发事务表现得像按照顺序一个一个执行。
这样:
text
脏读
不可重复读
幻读
都能够被避免。
但是代价就是:
text
并发能力降低
因为很多操作之间需要:
text
等待
加锁
阻塞
所以虽然隔离程度最高:
text
SERIALIZABLE
但实际系统中通常不会无脑使用。
25. 四种隔离级别放在一起
可以简单总结:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 避免 | 可能 | 可能 |
| REPEATABLE READ | 避免 | 避免 | SQL 标准下仍可能 |
| SERIALIZABLE | 避免 | 避免 | 避免 |
26. 怎么查看当前事务隔离级别?
MySQL 中可以执行:
sql
SELECT @@transaction_isolation;
例如可能返回:
text
REPEATABLE-READ
表示当前使用:
text
REPEATABLE READ
也可以设置当前 Session:
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
或者:
sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
27. Spring 中的事务其实也是这些东西
如果做 Java / Spring Boot 开发,经常会写:
java
@Transactional
public void transfer(
Long fromId,
Long toId,
BigDecimal amount) {
accountService.deduct(fromId, amount);
accountService.add(toId, amount);
}
这里:
text
@Transactional
本质上就是让:
text
deduct()
add()
在一个数据库事务中执行。
正常:
text
两个都成功
↓
COMMIT
发生异常:
text
中途失败
↓
ROLLBACK
所以理解 MySQL 事务之后,再看:
text
Spring @Transactional
就不会觉得它是什么神秘的东西。
Spring 只是:
text
帮助我们管理事务边界
真正的数据事务能力最终仍然来自数据库
28. 为什么 @Transactional 有时候不回滚?
很多 Java 开发者会遇到:
java
@Transactional
public void save() {
...
}
但是最终发现:
text
事务没有回滚
常见原因包括:
text
方法内部 this 调用
异常被自己 catch 掉
异常类型不符合默认回滚规则
方法没有经过 Spring 事务代理
这和Spring AOP有关。
不过这属于 Spring 事务层面的内容,不是 MySQL 事务本身。