知识模块
☕ Java 知识模块
七、数据库与JDBC
数据库事务隔离

数据库事务隔离

事务的 ACID 特性

特性说明实现机制
Atomicity(原子性)事务是不可分割的工作单位,要么全部成功,要么全部失败undo log(回滚日志)
Consistency(一致性)事务前后数据库的完整性约束没有被破坏由应用层和数据库共同保证
Isolation(隔离性)多个事务并发执行时,一个事务的执行不应影响其他事务锁机制 + MVCC
Durability(持久性)事务完成后,对数据库的修改是永久性的redo log(重做日志)

并发事务的问题

1. 脏读(Dirty Read)

一个事务读到了另一个事务未提交的数据。

时间线    事务A                    事务B
  1      BEGIN                   
  2      UPDATE user SET age=20  
         WHERE id=1 (age: 18→20)
  3                               SELECT age FROM user WHERE id=1
                                 → 读到 age=20(未提交的数据)
  4      ROLLBACK (age 回滚为18)  
  5                               事务B使用的 age=20 是脏数据!

2. 不可重复读(Non-Repeatable Read)

同一个事务内,两次读取同一数据得到不同结果(针对修改)。

时间线    事务A                    事务B
  1      BEGIN                   
  2      SELECT age FROM user    
         WHERE id=1 → age=18
  3                               BEGIN
                                  UPDATE user SET age=20 WHERE id=1
                                  COMMIT
  4      SELECT age FROM user    
         WHERE id=1 → age=20
         (同一事务内两次读取结果不同!)

3. 幻读(Phantom Read)

同一个事务内,两次查询返回的记录数不同(针对插入/删除)。

时间线    事务A                    事务B
  1      BEGIN                   
  2      SELECT * FROM user      
         WHERE age > 18 → 10条
  3                               BEGIN
                                  INSERT INTO user(age) VALUES(20)
                                  COMMIT
  4      SELECT * FROM user      
         WHERE age > 18 → 11条
         (多了一条"幻影"记录)

问题对比

问题产生原因影响范围
脏读读到未提交数据数据不一致,可能回滚
不可重复读读到已提交的修改同一数据值变化
幻读读到已提交的插入/删除结果集数量变化

四种隔离级别

MySQL InnoDB 默认隔离级别:REPEATABLE READ(可重复读)

隔离级别与问题关系

隔离级别脏读不可重复读幻读实现方式
READ UNCOMMITTED(读未提交)✅ 可能✅ 可能✅ 可能无特殊处理
READ COMMITTED(读已提交)❌ 不可能✅ 可能✅ 可能MVCC + 行锁
REPEATABLE READ(可重复读)❌ 不可能❌ 不可能✅ 可能*MVCC + 间隙锁
SERIALIZABLE(串行化)❌ 不可能❌ 不可能❌ 不可能表锁

*MySQL InnoDB 的 RR 级别通过间隙锁(Gap Lock)防止幻读

各级别详解

READ UNCOMMITTED(读未提交)

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
  • 最低级别,几乎不加锁
  • 性能最好,但数据一致性最差
  • 实际应用极少使用

READ COMMITTED(读已提交)- Oracle 默认

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 只能读到已提交的数据
  • 解决脏读问题
  • 每次查询生成新的 Read View

REPEATABLE READ(可重复读)- MySQL 默认

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  • 同一事务内多次读取结果一致
  • 解决脏读和不可重复读
  • 事务开始时生成 Read View,整个事务期间复用

SERIALIZABLE(串行化)

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • 最高隔离级别
  • 强制事务串行执行
  • 性能最差,并发能力最低
  • 通过对读取的每一行数据加锁实现

MVCC(多版本并发控制)

核心思想

MVCC 通过保存数据的历史版本,使读操作可以读取某个历史版本的数据,从而避免加锁。

InnoDB MVCC 实现

隐式字段

每行数据包含两个隐藏列:

字段说明
DB_TRX_ID最后插入或更新该行的事务 ID(6字节)
DB_ROLL_PTR回滚指针,指向 undo log(7字节)
DB_ROW_ID行 ID(6字节,如果表没有主键则自动创建)

undo log(回滚日志)

当前数据: id=1, age=20, DB_TRX_ID=100, DB_ROLL_PTR=0x7F8...

undo log 链:
┌─────────────────────────────────────┐
│ undo log (trx_id=100): age=20       │ ← 当前版本
├─────────────────────────────────────┤
│ undo log (trx_id=80): age=18        │ ← 历史版本1
├─────────────────────────────────────┤
│ undo log (trx_id=50): age=16        │ ← 历史版本2
└─────────────────────────────────────┘

Read View(读视图)

Read View 包含四个关键字段:

字段说明
m_ids创建 Read View 时活跃的事务 ID 列表
min_trx_id活跃事务中最小的事务 ID
max_trx_id下一个将要分配的事务 ID
creator_trx_id创建该 Read View 的事务 ID

可见性判断规则

对于某行数据的 DB_TRX_ID

  1. 等于 creator_trx_id:自己修改的,可见
  2. 小于 min_trx_id:事务已提交,可见
  3. 大于等于 max_trx_id:事务在 Read View 创建后才开启,不可见
  4. m_ids:事务未提交,不可见
  5. 不在 m_ids:事务已提交,可见

RC 和 RR 的区别

隔离级别Read View 生成时机效果
READ COMMITTED每次 SELECT 生成新的能读到最新已提交数据
REPEATABLE READ事务第一次 SELECT 时生成整个事务看到相同快照

面试高频问题

Q1: MySQL 如何解决幻读?

回答要点:

  1. 快照读(普通 SELECT):通过 MVCC 解决

    • RR 级别下,事务开始时生成 Read View
    • 后续 SELECT 只能看到 Read View 创建前已提交的数据
    • 新插入的数据对当前事务不可见
  2. 当前读(SELECT ... FOR UPDATE、UPDATE、DELETE):通过间隙锁解决

    • Next-Key Lock = Record Lock(行锁)+ Gap Lock(间隙锁)
    • 锁住记录及其前后间隙,防止其他事务插入
-- 当前读示例
SELECT * FROM user WHERE id > 5 FOR UPDATE;
-- 锁定 id > 5 的所有记录,以及 (5, +∞) 间隙
-- 其他事务无法在此范围插入新记录

Q2: RC 和 RR 本质区别是什么?

回答要点:

RC 每次查询生成新 Read View,RR 只在事务开始时生成一次。

-- 场景:事务A先查询,事务B修改并提交,事务A再查询
 
-- RC 隔离级别
事务A: SELECT age FROM user WHERE id=1;  -- age=18
事务B: UPDATE user SET age=20 WHERE id=1; COMMIT;
事务A: SELECT age FROM user WHERE id=1;  -- age=20(生成新Read View)
 
-- RR 隔离级别
事务A: SELECT age FROM user WHERE id=1;  -- age=18(生成Read View)
事务B: UPDATE user SET age=20 WHERE id=1; COMMIT;
事务A: SELECT age FROM user WHERE id=1;  -- age=18(复用Read View)

Q3: 为什么 MySQL 默认使用 RR 而不是 RC?

回答要点:

  1. 数据一致性更强:避免不可重复读问题

  2. binlog 日志格式:MySQL binlog 有三种格式

    • STATEMENT:SQL 语句(RR 首选)
    • ROW:行数据变化
    • MIXED:混合模式

    RR + STATEMENT 格式可以保证主从复制一致性

  3. 历史原因:MySQL 5.1 之前,RC + STATEMENT 可能导致主从不一致

Q4: 什么是当前读和快照读?

类型说明示例
快照读读取历史版本数据,不加锁SELECT * FROM table
当前读读取最新版本数据,加锁SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
INSERT / UPDATE / DELETE

Q5: MVCC 能解决所有并发问题吗?

回答要点:

不能。MVCC 只解决读-写冲突,写-写冲突仍需要锁。

场景:两个事务同时更新同一行

事务A: UPDATE user SET age=age+1 WHERE id=1;  -- 获取行锁
事务B: UPDATE user SET age=age+1 WHERE id=1;  -- 等待行锁
事务A: COMMIT;  -- 释放行锁
事务B: 获取行锁,执行更新  -- 不会读到旧版本,而是等待

总结

隔离级别脏读不可重复读幻读MVCC Read View锁机制
READ UNCOMMITTED不使用
READ COMMITTED×每次 SELECT 生成行锁
REPEATABLE READ××√*事务开始时生成行锁 + 间隙锁
SERIALIZABLE×××不使用表锁

MySQL InnoDB 的 RR 级别通过间隙锁防止幻读

选择建议:

  • 大多数场景:使用默认的 RR 级别
  • 需要读取最新数据:使用 RC 级别
  • 对一致性要求极高:使用 SERIALIZABLE(性能下降明显)