Skip to content

定义 ​

事务隔离级别 (Transaction Isolation Level) 是 SQL 标准定义的事务隔离程度等级,用于在并发性能和数据一致性之间做出权衡。隔离级别越高,数据一致性越好,但并发性能越低。

SQL 标准定义了四种隔离级别(从低到高):

  1. READ UNCOMMITTED(读未提交)
  2. READ COMMITTED(读已提交)
  3. REPEATABLE READ(可重复读)
  4. 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 InnoDBREPEATABLE READ通过 MVCC + Next-Key Lock 实现
PostgreSQLREAD COMMITTED通过 MVCC 实现
OracleREAD COMMITTED通过 Undo Segments 实现
SQL ServerREAD COMMITTED通过锁机制实现
SQLiteSERIALIZABLE单文件锁,天然串行

选择隔离级别的策略 ​

策略一:根据业务需求 ​

高并发 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. 原子性:要么都成功,要么都失败

最佳实践 ​

  1. 优先使用数据库默认级别:
  • MySQL: REPEATABLE READ
  • PostgreSQL: READ COMMITTED
  1. 避免使用 READ UNCOMMITTED:除非有非常明确的理由

  2. 谨慎使用 SERIALIZABLE:性能开销太大

  3. 使用 FOR UPDATE 代替提升隔离级别:

sql
-- 推荐
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 不推荐
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  1. 保持事务简短:事务越短,隔离问题越少

  2. 监控长事务:长事务容易导致锁竞争和隔离问题

  3. 理解 MVCC:不同隔离级别在 MVCC 下的行为差异

关联术语 ​

  • [[MVCC]]
  • [[ACID]]
  • [[脏读]]
  • [[幻读]]
  • [[死锁]]

参考资料 ​

Released under MIT License.