定义
事务隔离级别 (Transaction Isolation Level) 是 SQL 标准定义的事务隔离程度等级,用于在并发性能和数据一致性之间做出权衡。隔离级别越高,数据一致性越好,但并发性能越低。
SQL 标准定义了四种隔离级别(从低到高):
- READ UNCOMMITTED(读未提交)
- READ COMMITTED(读已提交)
- REPEATABLE READ(可重复读)
- SERIALIZABLE(串行化)
详细笔记
核心原理
并发问题
在讨论隔离级别之前,先了解三种典型的并发问题:
一、脏读 (Dirty Read)
事务 A: START TRANSACTION;
事务 A: UPDATE users SET name = '新名' WHERE id = 1; -- 未提交
事务 B: SELECT * FROM users WHERE id = 1;
→ 读到: '新名' ❌ 脏读!
→ 如果事务 A 回滚,事务 B 读到的就是无效数据
事务 A: ROLLBACK; -- 回滚二、不可重复读 (Non-Repeatable Read)
事务 A: START TRANSACTION;
事务 A: SELECT * FROM users WHERE id = 1;
→ 读到: name = '张三'
事务 B: START TRANSACTION;
事务 B: UPDATE users SET name = '李四' WHERE id = 1;
事务 B: COMMIT;
事务 A: SELECT * FROM users WHERE id = 1;
→ 读到: name = '李四' ❌ 与第一次读取不一致!
-- 问题:同一事务内,两次相同查询结果不同三、幻读 (Phantom Read)
事务 A: START TRANSACTION;
事务 A: SELECT COUNT(*) FROM users WHERE age > 20;
→ 结果: 100
事务 B: INSERT INTO users (id, name, age) VALUES (101, '新人', 25);
事务 B: COMMIT;
事务 A: SELECT COUNT(*) FROM users WHERE age > 20;
→ 结果: 101 ❌ 多了一行"幻影"记录!
-- 问题:同一事务内,范围查询的结果集发生变化四种隔离级别详解
一、READ UNCOMMITTED(读未提交)
隔离程度: 最低
并发性能: 最高
sql
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 或使用
SET SESSION tx_isolation = 'READ-UNCOMMITTED';特点:
- ✅ 并发性能最高
- ❌ 存在脏读
- ❌ 存在不可重复读
- ❌ 存在幻读
示例:
sql
-- 会话 A
START TRANSACTION;
UPDATE users SET balance = 900 WHERE id = 1; -- 原余额 1000
-- 会话 B(READ UNCOMMITTED)
SELECT balance FROM users WHERE id = 1;
-- 读到: 900 ❌ 脏读!
-- 会话 A
ROLLBACK; -- 回滚
-- 会话 B
SELECT balance FROM users WHERE id = 1;
-- 读到: 1000 -- 刚才的 900 是无效的!适用场景:
- 几乎不使用
- 仅在对数据一致性要求极低、追求极致性能的场景
二、READ COMMITTED(读已提交)
隔离程度: 中等偏低
并发性能: 高
sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- MySQL
SET SESSION tx_isolation = 'READ-COMMITTED';
-- PostgreSQL(默认级别)
-- Oracle(默认级别)特点:
- ✅ 避免脏读
- ❌ 存在不可重复读
- ❌ 存在幻读
- ✅ 并发性能较高
实现机制(MVCC):
- 每次 SELECT 都创建新的 Read View
- 只能看到已提交的版本
示例:
sql
-- 会话 A
START TRANSACTION;
-- 会话 B
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;
-- 读到: 1000
-- 会话 A
UPDATE users SET balance = 900 WHERE id = 1;
COMMIT;
-- 会话 B
SELECT balance FROM users WHERE id = 1;
-- 读到: 900 ✅ 看到了已提交的更改
-- ❌ 但这就是"不可重复读"
COMMIT;适用场景:
- 大多数 OLTP 系统
- PostgreSQL、Oracle 的默认级别
- 对一致性要求不是特别高的业务
三、REPEATABLE READ(可重复读)
隔离程度: 中等偏高
并发性能: 中等
sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- MySQL(默认级别)
SET SESSION tx_isolation = 'REPEATABLE-READ';特点:
- ✅ 避免脏读
- ✅ 避免不可重复读
- ⚠️ 部分避免幻读(MySQL 通过 Next-Key Lock)
- ✅ 并发性能中等
实现机制(MVCC):
- 第一次 SELECT 时创建 Read View
- 之后复用同一个 Read View
- 整个事务看到的是同一份快照
示例:
sql
-- 会话 A
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;
-- 读到: 1000
-- 创建 Read View A
-- 会话 B
START TRANSACTION;
UPDATE users SET balance = 900 WHERE id = 1;
COMMIT;
-- 会话 A
SELECT balance FROM users WHERE id = 1;
-- 读到: 1000 ✅ 与第一次一致(可重复读)
-- 因为复用 Read View A,看不到事务 B 的提交
COMMIT;
-- 会话 A 再次查询(新事务)
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;
-- 读到: 900 ✅ 看到最新已提交版本MySQL 的特殊优化:避免幻读
MySQL InnoDB 在 RR 级别下,通过 Next-Key Lock(间隙锁 + 记录锁)来避免大部分幻读:
sql
-- 会话 A
START TRANSACTION;
SELECT * FROM users WHERE age > 20 LOCK IN SHARE MODE;
-- 锁定:age > 20 的记录 + 间隙
-- 会话 B
INSERT INTO users (id, name, age) VALUES (101, '新人', 25);
-- 被阻塞!等待会话 A 释放锁
-- 会话 A
COMMIT;
-- 会话 B
-- 获得锁,插入成功注意:只有在当前读(LOCK IN SHARE MODE / FOR UPDATE)时才加间隙锁,快照读(普通 SELECT)仍然可能幻读。
适用场景:
- MySQL 默认级别
- 对一致性要求较高的业务
- 需要保证报表数据一致性的场景
四、SERIALIZABLE(串行化)
隔离程度: 最高
并发性能: 最低
sql
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;特点:
- ✅ 避免脏读
- ✅ 避免不可重复读
- ✅ 避免幻读
- ❌ 并发性能最低
实现机制:
- 所有 SELECT 都隐式加锁
- 事务完全串行执行
- 退化为传统的 2PL(两阶段锁)协议
示例:
sql
-- 会话 A
START TRANSACTION;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM users WHERE age > 20;
-- 对扫描的记录加共享锁
-- 会话 B
INSERT INTO users (id, name, age) VALUES (101, '新人', 25);
-- 被阻塞!必须等待会话 A 提交
-- 会话 A
COMMIT;
-- 会话 B
-- 获得锁,插入成功适用场景:
- 极少使用
- 对数据一致性要求极高的金融系统
- 数据仓库的 ETL 过程
隔离级别对比表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 | 典型应用 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | ❌ | ❌ | ❌ | 最高 | 几乎不用 |
| READ COMMITTED | ✅ | ❌ | ❌ | 高 | PostgreSQL, Oracle |
| REPEATABLE READ | ✅ | ✅ | ⚠️部分 | 中 | MySQL(默认) |
| SERIALIZABLE | ✅ | ✅ | ✅ | 最低 | 金融系统 |
✅ = 避免 ❌ = 存在 ⚠️ = 部分避免
各数据库的默认隔离级别
| 数据库 | 默认级别 | 说明 |
|---|---|---|
| MySQL InnoDB | REPEATABLE READ | 通过 MVCC + Next-Key Lock 实现 |
| PostgreSQL | READ COMMITTED | 通过 MVCC 实现 |
| Oracle | READ COMMITTED | 通过 Undo Segments 实现 |
| SQL Server | READ COMMITTED | 通过锁机制实现 |
| SQLite | SERIALIZABLE | 单文件锁,天然串行 |
选择隔离级别的策略
策略一:根据业务需求
高并发 Web 应用:
→ READ COMMITTED
→ 理由:性能好,避免脏读即可
金融交易系统:
→ REPEATABLE READ 或 SERIALIZABLE
→ 理由:强一致性要求
报表系统:
→ REPEATABLE READ
→ 理由:保证报表期间数据一致
数据分析:
→ READ COMMITTED
→ 理由:可以接受轻微不一致,追求性能策略二:根据数据库特性
MySQL:
→ 保持默认 REPEATABLE READ
→ 除非有明确理由,否则不修改
PostgreSQL:
→ 保持默认 READ COMMITTED
→ 需要时可提升到 REPEATABLE READ
Oracle:
→ 保持默认 READ COMMITTED
→ Oracle 不支持 RR 级别策略三:混合使用
sql
-- 大部分查询使用默认级别
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
COMMIT;
-- 关键业务使用更高级别
START TRANSACTION;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 执行关键操作
SELECT ... FOR UPDATE;
UPDATE ...
COMMIT;监控与诊断
MySQL:查看当前隔离级别
sql
-- 查看全局隔离级别
SELECT @@GLOBAL.tx_isolation;
-- 或 MySQL 8.0+
SELECT @@GLOBAL.transaction_isolation;
-- 查看会话隔离级别
SELECT @@SESSION.tx_isolation;
-- 或 MySQL 8.0+
SELECT @@SESSION.transaction_isolation;
-- 输出:
-- +-------------------------+
-- | @@GLOBAL.tx_isolation |
-- +-------------------------+
-- | REPEATABLE-READ |
-- +-------------------------+查看事务状态
sql
-- 查看所有活跃事务
SELECT
trx_id,
trx_state,
trx_started,
trx_isolation_level,
trx_rows_locked,
trx_query
FROM information_schema.INNODB_TRX;
-- 输出:
-- +--------+-----------+---------------------+---------------------+---------------+------------------+
-- | trx_id | trx_state | trx_started | trx_isolation_level | trx_rows_locked| trx_query |
-- +--------+-----------+---------------------+---------------------+---------------+------------------+
-- | 12345 | RUNNING | 2024-01-15 10:30:00 | REPEATABLE READ | 5 | SELECT ... |
-- +--------+-----------+---------------------+---------------------+---------------+------------------+实际案例
案例一:电商库存扣减
sql
-- 问题:超卖
-- 错误实现(READ COMMITTED 级别)
START TRANSACTION;
-- 步骤一:查询库存
SELECT stock FROM products WHERE id = 1;
-- 读到: stock = 10
-- 此时,其他 10 个事务也读到 stock = 10
-- 步骤二:扣减库存
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- 结果:10 个事务都成功扣减,stock 变为 0
-- 但实际只卖出 1 件,超卖 9 件!❌
-- 正确实现(使用 FOR UPDATE)
START TRANSACTION;
-- 当前读,加排他锁
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 读到: stock = 10
-- 其他事务的 SELECT ... FOR UPDATE 被阻塞
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- 下一个事务才能继续
-- 或使用更高隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;案例二:银行转账
sql
-- 银行转账必须使用强隔离级别
DELIMITER $$
CREATE PROCEDURE transfer(
from_account INT,
to_account INT,
amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 锁定两个账户
SELECT balance FROM accounts WHERE id = from_account FOR UPDATE;
SELECT balance FROM accounts WHERE id = to_account FOR UPDATE;
-- 检查余额
IF (SELECT balance FROM accounts WHERE id = from_account) >= amount THEN
-- 扣款
UPDATE accounts SET balance = balance - amount WHERE id = from_account;
-- 收款
UPDATE accounts SET balance = balance + amount WHERE id = to_account;
COMMIT;
ELSE
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
END IF;
END$$
DELIMITER ;
-- 保证:
-- 1. 不会脏读
-- 2. 不会不可重复读
-- 3. 不会幻读
-- 4. 原子性:要么都成功,要么都失败最佳实践
- 优先使用数据库默认级别:
- MySQL: REPEATABLE READ
- PostgreSQL: READ COMMITTED
避免使用 READ UNCOMMITTED:除非有非常明确的理由
谨慎使用 SERIALIZABLE:性能开销太大
使用 FOR UPDATE 代替提升隔离级别:
sql
-- 推荐
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 不推荐
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;保持事务简短:事务越短,隔离问题越少
监控长事务:长事务容易导致锁竞争和隔离问题
理解 MVCC:不同隔离级别在 MVCC 下的行为差异
关联术语
- [[MVCC]]
- [[ACID]]
- [[脏读]]
- [[幻读]]
- [[死锁]]
参考资料
- SQL 标准: ISO/IEC 9075-2:2016
- MySQL 官方文档: Transaction Isolation Levels
- PostgreSQL 文档: Transaction Isolation
- 《高性能 MySQL》第 7 章:事务与锁