高级 #database#isolation#transaction

事务隔离级别

隔离级别在并发性能和正确性之间做权衡——级别越高数据越安全,但并发性能越低。脏读、不可重复读、幻读是隔离级别要解决的三个问题

🎭 完全的隔离性太昂贵了

上一节我们学了 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;

代价:并发性能最低——因为冲突概率高,事务经常要等待或重试。

适用场景:金融系统、库存管理等对数据一致性要求极高的场景。


🌍 不同数据库的默认隔离级别

数据库默认隔离级别原因
PostgreSQLREAD COMMITTED适用大多数场景,性能好
MySQL InnoDBREPEATABLE READ历史原因(binlog 复制兼容性)
OracleREAD COMMITTED同样的原因——默认安全高效
SQL ServerREAD 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 是如何工作的。