MySQL事务隔离级别的四重奏 🎭

52 阅读12分钟

一、开篇故事:图书馆的四种借阅规则 📚

想象你在一个魔法图书馆,有四种不同的借阅规则:

规则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次查询:账户11000元
    第2次查询:账户1900元 ← 别人转走了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无隔离⭐⭐⭐⭐⭐
RCMVCC(每次新视图)⭐⭐⭐⭐
RR⚠️MVCC+锁⭐⭐⭐
S全加锁

6.2 性能对比

并发性能(高→低):
  RU > RC > RR > SERIALIZABLE

数据一致性(高→低):
  SERIALIZABLE > RR > RC > RU

6.3 使用场景

隔离级别适用场景典型数据库
RU几乎不用-
RCOLTP高并发场景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隔离级别下防止超卖?

答:

  1. 使用乐观锁(version字段)
  2. 使用SELECT FOR UPDATE(加排他锁)
  3. 使用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

愿你的事务永远一致! 🎯✨