一、开篇故事:图书馆的四种借阅规则 📚
想象你在一个魔法图书馆,有四种不同的借阅规则:
规则1:READ UNCOMMITTED(读未提交)—— 无秩序图书馆
场景:
小明正在改《MySQL指南》第100页的内容(还没保存)
小红走过来,看到了小明改了一半的内容
小明突然撤销修改(回滚)
小红:???我刚才看的是啥?
问题:读到了未提交的数据(脏读)💩
规则2:READ COMMITTED(读已提交)—— 基本秩序图书馆
场景:
小明看《MySQL指南》第100页:价格 = 100元
小红修改价格为200元并保存
小明再看第100页:价格 = 200元
小明:???刚才不是100元吗?
问题:同一事务中,两次读取不一致(不可重复读)🔄
规则3:REPEATABLE READ(可重复读)—— MySQL默认规则
场景:
小明开始借阅《MySQL指南》
小明看到有10本《Python入门》
小红新增了5本《Python入门》并上架
小明再看:还是10本《Python入门》
小明借走1本《Python入门》
小明再看:居然有14本?!
问题:修改时出现了幻读(但查询时不会)👻
规则4:SERIALIZABLE(可串行化)—— 严格秩序图书馆
场景:
小明开始借阅《MySQL指南》
小红想改《MySQL指南》
管理员:"等小明看完再说!"
小红:😤 等待...
问题:性能极差,但完全隔离 🐌
二、什么是事务隔离级别?🤔
2.1 定义
事务隔离级别(Transaction Isolation Level) 定义了一个事务中所做的修改,在何种程度上对其他事务可见。
2.2 四大隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ❌ 会 | ❌ 会 | ❌ 会 |
| READ COMMITTED | ✅ 不会 | ❌ 会 | ❌ 会 |
| REPEATABLE READ | ✅ 不会 | ✅ 不会 | ⚠️ 部分解决 |
| SERIALIZABLE | ✅ 不会 | ✅ 不会 | ✅ 不会 |
2.3 隔离级别强度
READ UNCOMMITTED(最弱)
↓
READ COMMITTED
↓
REPEATABLE READ(MySQL默认)
↓
SERIALIZABLE(最强)
隔离性越强 → 并发性越差 → 性能越低
三、三大并发问题详解 💥
3.1 脏读(Dirty Read)
定义: 读到了其他事务未提交的数据。
示例:
-- 事务A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 还未提交!
-- 事务B(READ UNCOMMITTED隔离级别)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 读到了事务A未提交的修改!💩
-- 事务A回滚
ROLLBACK;
-- 事务B读到的数据是无效的(脏数据)
图解:
时间线:
t1: 事务A: balance = 1000
t2: 事务A: UPDATE balance = 900(未提交)
t3: 事务B: SELECT balance → 900 ❌ 脏读!
t4: 事务A: ROLLBACK(900变回1000)
t5: 事务B: 以为balance是900,实际是1000
危害:
转账场景:
小明账户:1000元
小红账户:1000元
事务A:小明转账给小红100元
→ 小明账户:900元(未提交)
事务B:查询小明账户
→ 读到900元(脏读)❌
→ 基于900元做业务逻辑
事务A:ROLLBACK
→ 小明账户:1000元
事务B:使用的900元是脏数据,业务逻辑错误!💥
3.2 不可重复读(Non-Repeatable Read)
定义: 同一事务中,多次读取同一数据返回不同的结果。
示例:
-- 事务A(READ COMMITTED隔离级别)
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 结果:1000
-- 事务B
BEGIN;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT; -- 提交了!
-- 事务A
SELECT balance FROM accounts WHERE id = 1; -- 结果:900 ❌
-- 咦?刚才不是1000吗?
COMMIT;
图解:
时间线:
t1: 事务A: SELECT balance → 1000
t2: 事务B: UPDATE balance = 900
t3: 事务B: COMMIT
t4: 事务A: SELECT balance → 900 ❌ 不一致!
危害:
统计场景:
事务A:统计所有账户总余额
第1次查询:账户1有1000元
第2次查询:账户1有900元 ← 别人转走了100元
结果:同一个事务中,两次查询不一致!
如果基于第1次查询做决策,可能出错!
3.3 幻读(Phantom Read)
定义: 同一事务中,多次查询返回的结果集不同(新增或删除了行)。
示例:
-- 事务A(REPEATABLE READ隔离级别)
BEGIN;
SELECT * FROM users WHERE age > 20; -- 结果:3条记录
-- 事务B
BEGIN;
INSERT INTO users VALUES (4, '李四', 25);
COMMIT;
-- 事务A(快照读,看不到新数据)
SELECT * FROM users WHERE age > 20; -- 结果:还是3条 ✅
-- 事务A(当前读,能看到新数据)
UPDATE users SET name = '修改' WHERE age > 20;
-- 实际修改了4条!(包括李四)❌
SELECT * FROM users WHERE age > 20; -- 结果:4条 👻 幻读!
COMMIT;
图解:
时间线:
t1: 事务A: SELECT COUNT(*) → 3条
t2: 事务B: INSERT 1条
t3: 事务B: COMMIT
t4: 事务A: SELECT COUNT(*) → 还是3条(快照读)✅
t5: 事务A: UPDATE(当前读)→ 修改了4条 ❌
t6: 事务A: SELECT COUNT(*) → 4条 👻 出现幻行!
危害:
统计场景:
事务A:查询年龄>20的用户数量 → 3个
事务B:插入1个年龄25的用户
事务A:
基于"有3个用户"做业务逻辑
修改这3个用户的数据
结果修改了4个用户!
→ 出现了"幻影"用户 👻
四、四大隔离级别详解 🎯
4.1 READ UNCOMMITTED(读未提交)
特点:
- 可以读到其他事务未提交的数据
- 没有任何隔离
- 性能最高,但几乎不用
实现:
不加锁,直接读取最新数据
使用场景:
❌ 几乎不使用
⚠️ 仅用于对数据一致性要求极低的场景(如日志统计)
示例:
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT * FROM accounts; -- 能看到其他事务未提交的修改
COMMIT;
4.2 READ COMMITTED(读已提交)
特点:
- 只能读到已提交的数据
- 避免了脏读
- 每次查询都创建新的Read View(MVCC)
- Oracle、PostgreSQL默认隔离级别
实现:
MVCC:每次查询创建新的Read View
→ 能读到其他事务已提交的修改
→ 不能读到未提交的修改
优点:
✅ 避免脏读
✅ 并发性好
✅ 适合大多数OLTP场景
缺点:
❌ 不可重复读
❌ 幻读
示例:
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 事务A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- 事务B修改并提交
-- ...
SELECT balance FROM accounts WHERE id = 1; -- 900 ← 不一致!
COMMIT;
4.3 REPEATABLE READ(可重复读)
特点:
- MySQL InnoDB默认隔离级别 ⭐⭐⭐⭐⭐
- 避免了脏读和不可重复读
- 事务开始时创建Read View,之后一直复用
- 部分解决了幻读(快照读不会幻读,当前读会)
实现:
MVCC + Next-Key Lock
- 快照读:使用MVCC,复用Read View
- 当前读:使用Next-Key Lock防止幻读
优点:
✅ 避免脏读
✅ 避免不可重复读
✅ 快照读避免幻读
✅ 性能和隔离性平衡
缺点:
⚠️ 当前读还是会幻读(但可以通过锁解决)
示例:
-- MySQL默认就是REPEATABLE READ
BEGIN;
-- 创建Read View
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- 即使其他事务修改并提交,这里看到的还是1000
SELECT balance FROM accounts WHERE id = 1; -- 1000 ✅
COMMIT;
4.4 SERIALIZABLE(可串行化)
特点:
- 最高隔离级别
- 完全避免脏读、不可重复读、幻读
- 所有读操作都加共享锁
- 性能最差,几乎不用
实现:
所有SQL都加锁:
- SELECT:加共享锁(S锁)
- INSERT/UPDATE/DELETE:加排他锁(X锁)
- 读写互斥
优点:
✅ 完全隔离,100%一致性
✅ 避免所有并发问题
缺点:
❌ 性能极差
❌ 并发度极低
❌ 容易死锁
示例:
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 事务A
BEGIN;
SELECT * FROM accounts WHERE id = 1; -- 加共享锁
-- 事务B
UPDATE accounts SET balance = 900 WHERE id = 1; -- 等待事务A释放锁 😴
-- 事务A提交后,事务B才能执行
五、MySQL隔离级别的实现原理 🔍
5.1 MVCC实现(RC和RR)
READ COMMITTED:
每次SELECT都创建新的Read View
→ 能看到其他事务已提交的修改
REPEATABLE READ:
事务开始时创建Read View,之后复用
→ 看不到其他事务已提交的修改(快照读)
图解:
RC(每次创建新Read View):
t1: 事务A: SELECT → 创建ReadView1 → 看到1000
t2: 事务B: UPDATE 900并提交
t3: 事务A: SELECT → 创建ReadView2 → 看到900 ❌
RR(复用Read View):
t1: 事务A: SELECT → 创建ReadView → 看到1000
t2: 事务B: UPDATE 900并提交
t3: 事务A: SELECT → 复用ReadView → 还是1000 ✅
5.2 锁实现(SERIALIZABLE)
所有SELECT都加锁:
SELECT * FROM users WHERE id = 1;
↓
自动转换为:
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
六、隔离级别对比总结 📊
6.1 功能对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 | 并发性 |
|---|---|---|---|---|---|
| RU | ❌ | ❌ | ❌ | 无隔离 | ⭐⭐⭐⭐⭐ |
| RC | ✅ | ❌ | ❌ | MVCC(每次新视图) | ⭐⭐⭐⭐ |
| RR | ✅ | ✅ | ⚠️ | MVCC+锁 | ⭐⭐⭐ |
| S | ✅ | ✅ | ✅ | 全加锁 | ⭐ |
6.2 性能对比
并发性能(高→低):
RU > RC > RR > SERIALIZABLE
数据一致性(高→低):
SERIALIZABLE > RR > RC > RU
6.3 使用场景
| 隔离级别 | 适用场景 | 典型数据库 |
|---|---|---|
| RU | 几乎不用 | - |
| RC | OLTP高并发场景 | Oracle、PostgreSQL |
| RR | 需要可重复读的场景 | MySQL(默认) |
| SERIALIZABLE | 金融、账务系统 | 极少使用 |
七、实战案例:电商库存扣减 🛒
案例背景
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50),
stock INT -- 库存
);
INSERT INTO products VALUES (1, 'iPhone', 100);
场景1:READ UNCOMMITTED(危险)
-- 事务A:扣减库存
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id = 1;
-- 还未提交!
-- 事务B:查询库存
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT stock FROM products WHERE id = 1; -- 99 ❌ 脏读
-- 基于99做业务逻辑
-- 事务A:回滚
ROLLBACK;
-- 事务B的业务逻辑基于错误数据!💥
场景2:READ COMMITTED(可能超卖)
-- 事务A:检查库存并扣减
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT stock FROM products WHERE id = 1; -- 1(最后1个)
-- 判断:库存充足,可以卖
-- 事务B:同时也在扣减
BEGIN;
SELECT stock FROM products WHERE id = 1; -- 1(也看到1个)
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT; -- stock = 0
-- 事务A:继续扣减
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT; -- stock = -1 ❌ 超卖了!
场景3:REPEATABLE READ(正确)
-- 事务A:使用当前读(加锁)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 加排他锁
-- stock = 1
-- 事务B:被阻塞
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 等待... 😴
-- 事务A:扣减并提交
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT; -- stock = 0
-- 事务B:现在可以执行了
SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 0
-- 判断:库存不足,不能卖 ✅
ROLLBACK;
解决方案:乐观锁
-- 添加版本号
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
-- 事务A
BEGIN;
SELECT stock, version FROM products WHERE id = 1;
-- stock = 1, version = 10
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10; -- 只有version没变才能更新
-- 如果version已经被别人改了,UPDATE失败
-- 重试即可
COMMIT;
八、如何设置和查看隔离级别?⚙️
8.1 查看当前隔离级别
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 查看会话隔离级别
SELECT @@session.transaction_isolation;
SELECT @@transaction_isolation; -- 简写
-- 结果示例:
-- REPEATABLE-READ(MySQL默认)
8.2 设置隔离级别
-- 设置全局隔离级别(影响所有新连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置会话隔离级别(只影响当前连接)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置下一个事务的隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
8.3 配置文件设置
# my.cnf
[mysqld]
transaction-isolation = READ-COMMITTED
九、面试高频问题 🎤
Q1: MySQL的默认隔离级别是什么?
答: REPEATABLE READ(可重复读)。 Oracle和PostgreSQL默认是READ COMMITTED。
Q2: 四种隔离级别分别解决什么问题?
答:
- RU:不解决任何问题
- RC:解决脏读
- RR:解决脏读 + 不可重复读
- SERIALIZABLE:解决所有问题(脏读 + 不可重复读 + 幻读)
Q3: REPEATABLE READ是如何实现的?
答: 通过MVCC + Next-Key Lock:
- 快照读:使用MVCC,事务开始时创建Read View并复用
- 当前读:使用Next-Key Lock防止幻读
Q4: 为什么MySQL默认是RR而不是RC?
答: 历史原因,早期MySQL的binlog是statement格式,RC隔离级别下可能导致主从数据不一致。RR隔离级别配合binlog能保证主从一致。现在有了row格式binlog,RC也可以了。
Q5: 如何在RC隔离级别下防止超卖?
答:
- 使用乐观锁(version字段)
- 使用SELECT FOR UPDATE(加排他锁)
- 使用Redis预减库存
十、最佳实践 💡
1. 选择合适的隔离级别
高并发OLTP系统:RC
→ Oracle、PostgreSQL的选择
→ 并发性好
需要可重复读:RR
→ MySQL默认
→ 适合报表、统计
金融系统:SERIALIZABLE或RR + 锁
→ 数据一致性要求高
2. 避免长事务
-- ❌ 不好:长事务
BEGIN;
SELECT * FROM orders; -- 创建Read View
sleep(3600); -- 睡1小时
SELECT * FROM orders; -- 复用Read View
COMMIT;
-- 问题:Undo Log堆积,MVCC性能下降
-- ✅ 好:短事务
BEGIN;
SELECT * FROM orders;
-- 快速处理
COMMIT;
3. 读写分离 + 不同隔离级别
主库(写):REPEATABLE READ
→ 保证数据一致性
从库(读):READ COMMITTED
→ 提高并发性
→ 及时看到最新数据
4. 敏感操作使用当前读
-- ❌ 不好:快照读(可能看到旧数据)
BEGIN;
SELECT stock FROM products WHERE id = 1; -- 快照读
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- ✅ 好:当前读(看到最新数据)
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 当前读+加锁
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
十一、总结口诀 📝
隔离级别有四种,
RU、RC、RR、S。
RU几乎不使用,
脏读幻读都会有。
RC解决脏读问题,
Oracle默认它最好。
RR是MySQL默认,
可重复读不会跑。
SERIALIZABLE最严格,
所有问题都解决。
性能太差少使用,
金融场景才需要。
MVCC实现RC、RR,
Read View是关键。
RC每次都新建,
RR复用保一致。
选对级别很重要,
并发一致要权衡。
短事务是王道,
长事务要避免!
参考资料 📚
下期预告: 140-MySQL的Gap Lock、Next-Key Lock的作用和加锁规则 🔐
编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0
愿你的事务永远一致! 🎯✨