事务隔离级别
隔离级别在并发性能和正确性之间做权衡——级别越高数据越安全,但并发性能越低。脏读、不可重复读、幻读是隔离级别要解决的三个问题
🎭 完全的隔离性太昂贵了
上一节我们学了 ACID 中的 I(隔离性,Isolation)——保证事务之间互不干扰。
但这里有一个”价格标签”的问题:隔离性越强,同时处理的事务数量就越少。
想象一下:
- 最严格的方式(串行化):一次只允许一个事务运行——绝对安全,但一次只能处理一个人,完全失去并发能力
- 最宽松的方式(无隔离):所有事务都可以同时运行——速度最快,但数据可能一塌糊涂
数据库提供了四个级别的”隔离强度”,让你在安全性和性能之间做选择——这和生活中很多事情一样:更高的安全 = 更低的效率。
🏪 类比:食堂打饭的三种方式
隔离级别 READ UNCOMMITTED = 食堂没排队,大家一窝蜂挤在窗口前——速度最快,但可能打到别人碗里的菜(数据混乱)
隔离级别 SERIALIZABLE = 食堂单行排队,一次只服务一个人——绝对公平不冲突,但队伍很长很慢
隔离级别 READ COMMITTED = 食堂排成几个队——有些乱但大体有序,是大多数人的选择
数据库的四种隔离级别,就是在这条”安全-性能”的坐标轴上的四个刻度。
🚫 并发事务的三大问题
在了解隔离级别之前,先看看如果不隔离会发生什么——这三个问题隔离级别就是为了解决它们的。
① 脏读(Dirty Read)——读到未提交的数据
-- 事务 A:转账 100 元
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 余额变为 900
-- 🔴 还没 COMMIT!
-- 事务 B:查余额
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 读到 900(脏数据!)
COMMIT;
-- 事务 A:ROLLBACK(因为发现转错人了)
ROLLBACK; -- 余额回到 1000
-- 问题:事务 B 读到的 900 是"不存在"的数据——A 已经回滚了
脏读 = 读到了其他事务尚未提交的数据。这些数据可能被回滚——所以你读到的是”从未真实存在过”的数据。
② 不可重复读(Non-Repeatable Read)——同一事务两次读同一行结果不同
-- 事务 A:查看账户余额
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 第一次读:1000
-- 事务 B:修改余额并提交
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 事务 A:再次查看
SELECT balance FROM accounts WHERE id = 1; -- 第二次读:500(不一样了!)
COMMIT;
不可重复读 = 在同一个事务中,两次读取同一行数据,结果不一样(因为其他事务修改并提交了)。
⚠️ 注意和脏读的区别:脏读读到的是未提交的数据,不可重复读读到的是已提交的数据。不可重复读”至少数据是真实的”——只是事务内的两次读不一致了。
③ 幻读(Phantom Read)——两次查询结果集不同
-- 事务 A:查看所有余额 > 500 的账户
BEGIN;
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- 第一次查:3 个
-- 事务 B:插入一条新账户,余额 600,然后提交
BEGIN;
INSERT INTO accounts VALUES (4, '赵六', 600);
COMMIT;
-- 事务 A:再次查看
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- 第二次查:4 个(多了一个"幻影")
COMMIT;
幻读 = 在同一个事务中,两次范围查询的结果集不一样(满足条件的行数变了)。
💡 一句话区分三个问题:
- 脏读:读到别人没提交的(可能回滚的)数据
- 不可重复读:同一行数据变来变去
- 幻读:多了一些原来没有的行(或者少了一些行)
📊 四种隔离级别
ISO SQL 标准定义了四种隔离级别,从低到高:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | ⚡ 最高 |
| READ COMMITTED | ❌ 杜绝 | ✅ 可能 | ✅ 可能 | 高 |
| REPEATABLE READ | ❌ 杜绝 | ❌ 杜绝 | ✅ 可能 | 中 |
| SERIALIZABLE | ❌ 杜绝 | ❌ 杜绝 | ❌ 杜绝 | 🐢 最低 |
-- 设置当前事务的隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置会话的默认隔离级别(以 MySQL 为例)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
① READ UNCOMMITTED(读未提交)
做了什么:基本不隔离——可以读到其他事务还没提交的数据。
问题:脏读、不可重复读、幻读都可能发生。
适用场景:几乎没有——除非你对数据的准确性完全不在乎(比如粗略的统计数字)。
⚠️ 实际使用:大多数数据库(如 PostgreSQL)甚至不允许设置这个级别——太危险了。
② READ COMMITTED(读已提交)
做了什么:只读其他事务已提交的数据。解决了脏读。
如何解决脏读:读操作只读取已提交的版本——如果其他事务正在修改某行,读操作看到的是这行之前已提交的旧值。
-- 事务 A 正在修改(未提交)
UPDATE accounts SET balance = 500 WHERE id = 1;
-- 事务 B 读取(READ COMMITTED 级别)
SELECT balance FROM accounts WHERE id = 1;
-- 读到的是 1000(修改前的值),而不是 500(未提交的值)
未解决的问题:不可重复读——事务 B 两次读同一行,第一次读到旧值,第二次可能读到其他事务提交后的新值。
适用场景:最常用的隔离级别。PostgreSQL 的默认级别,大部分应用的最佳选择。
③ REPEATABLE READ(可重复读)
做了什么:保证在同一个事务内,多次读取同一行数据结果一致。解决了脏读 + 不可重复读。
如何解决不可重复读:在事务开始时创建一份”快照”,事务内的所有读操作都基于这份快照——其他事务的修改不会影响当前事务看到的版本。
事务 T1 开始 → 创建快照(balance=1000)
↓
事务 T2 修改 balance=500 → 不影响 T1 的快照
↓
事务 T1 再次读取 → 仍然看到 1000(快照中的数据)
未解决的问题:幻读——范围查询时,如果有新行被插入,结果集会变化。
💡 注意: MySQL 的 InnoDB 在 REPEATABLE READ 级别通过
间隙锁(Gap Lock)实际上部分解决了幻读——它阻止了其他事务在某个范围内插入新行。但标准的 SQL 定义中,REPEATABLE READ 不解决幻读。
适用场景:需要事务内多次读取一致性的场景(如报表、统计查询)。
④ SERIALIZABLE(可串行化)
做了什么:最严格的隔离级别——事务好像”一个接一个”串行执行。解决了脏读 + 不可重复读 + 幻读。
如何解决幻读:
- 方式一(锁实现):锁定查询涉及的所有行和范围,其他事务无法插入/修改这些数据
- 方式二(SSI 实现,Serializable Snapshot Isolation):检测事务间的读写冲突,如有冲突就强制回滚其中一个
-- 在 SERIALIZABLE 级别下:
-- 事务 A 查询所有余额 > 500 的账户
BEGIN;
SELECT * FROM accounts WHERE balance > 500;
-- 事务 B 尝试插入新账户(余额 600):
INSERT INTO accounts VALUES (4, '赵六', 600);
-- 会被阻塞(锁方式)或报错回滚(SSI 方式)
COMMIT;
代价:并发性能最低——因为冲突概率高,事务经常要等待或重试。
适用场景:金融系统、库存管理等对数据一致性要求极高的场景。
🌍 不同数据库的默认隔离级别
| 数据库 | 默认隔离级别 | 原因 |
|---|---|---|
| PostgreSQL | READ COMMITTED | 适用大多数场景,性能好 |
| MySQL InnoDB | REPEATABLE READ | 历史原因(binlog 复制兼容性) |
| Oracle | READ COMMITTED | 同样的原因——默认安全高效 |
| SQL Server | READ COMMITTED | 同 PostgreSQL |
💡 实用建议:
- 大多数 Web 应用用默认隔离级别就够了
- 只是查询(没有写操作)时,用 READ COMMITTED 完全没问题
- 涉及”先查再写”的业务逻辑(如库存扣减、抢票),要注意隔离级别是否够用
- SERIALIZABLE 不是万能药——它通过”串行化”保证安全,但会让并发性能大幅下降
🗺️ 隔离级别选择指南
你的应用 = 读多写少(博客)? → READ COMMITTED 够用
你的应用 = 读少写多(日志)? → READ COMMITTED 够用(甚至可以考虑 READ UNCOMMITTED 做统计)
你的应用 = 读写都多(电商)? → REPEATABLE READ 更稳妥
你的应用 = 资金相关(银行)? → SERIALIZABLE 或应用层加锁
一条重要原则: 选能满足数据一致性要求的最宽松的隔离级别。不需要为了”绝对安全”使用 SERIALIZABLE——大多数情况下 READ COMMITTED 配合应用层逻辑已经足够。
📝 小结
| 概念 | 一句话 |
|---|---|
| 脏读 | 读到其他事务未提交的数据 |
| 不可重复读 | 同一事务内两次读同一行结果不同 |
| 幻读 | 同一事务内两次范围查询结果集不同 |
| READ UNCOMMITTED | 最低隔离——啥问题都可能发生 |
| READ COMMITTED | 解决脏读——默认最常用 |
| REPEATABLE READ | 解决脏读+不可重复读 |
| SERIALIZABLE | 最高隔离——像串行执行一样安全,但最慢 |
🎯 思考题:在一个电商库存系统中,两个用户同时下单购买最后一件商品。在 READ COMMITTED 级别下可能会出现什么问题?应该用哪种隔离级别或什么应用层方案来解决?
为什么先学这个? 隔离级别是”策略”,而实现这些策略的底层机制是锁协议与并发控制——下一节深入讨论锁和 MVCC 是如何工作的。