欢迎光临
我们一直在努力

Java开发必知必会的MySQL核心知识点(三)-深入理解:事务、锁与 MVCC

前两篇我们学会了 MySQL 怎么存数据、怎么查得快。但你有没有想过一个问题:当你和同事同时操作同一条数据时,数据库是怎么保证不出乱子的?

试试脑补这个场景:你在转账 100 元,系统先从你的账户扣 100,再给对方加 100。如果扣完 100 之后,服务器突然断电——这 100 元是凭空消失,还是会神奇地回到你的账户?

这就是本篇要解决的问题。事务、锁、MVCC 是 MySQL 中最"硬核"的模块,也是面试中区分初级和高级开发的分水岭。别被它们的名字吓到,我们一个一个来。


一、事务:让一组操作同生共死

1.1 事务是什么?

事务(Transaction)是一组要么全部成功、要么全部失败的数据库操作。它和我们日常理解的"一批活"很像。

用 Java 代码表示:

@Transactional // Spring 声明式事务
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountMapper.decrease(fromId, amount); // 扣钱
// 如果这里抛异常,扣的钱会自动退回
accountMapper.increase(toId, amount); // 加钱
}

@Transactional 注解告诉 Spring:这两个操作绑定成一个事务,如果任何一个失败,前面的操作也全部撤销。

1.2 ACID:事务的四个铁律

特性通俗解释数据库怎么做到的
原子性 (Atomicity) 要么全做,要么全不做 undo log(记录反操作,失败时回滚)
一致性 (Consistency) 数据始终满足业务规则 其他三个特性共同保证
隔离性 (Isolation) 并发事务互不干扰 MVCC + 锁(本篇后面重点讲)
持久性 (Durability) 提交后数据不丢失 redo log(崩溃后用它恢复)

小白理解技巧:把事务想象成 Git 的 commit——没 commit 之前,修改都在暂存区,随时可以撤销。COMMIT 就是 git push,提交了就永久保存了。ROLLBACK 就是 git reset –hard,回到修改前的状态。


了解事务的基本概念后,下一个问题自然浮现:如果两个事务同时操作同一份数据,会出什么问题?数据库提供了什么机制来应对?


二、隔离级别:解决并发的"度"

2.1 三种并发问题

假设有两个事务 T1 和 T2 同时运行,可能出现三种问题:

脏读(Dirty Read)

T1:修改 name='张三' → '李四',还没提交
T2:读到了 '李四'           ← 脏读!
T1:回滚,name 还是 '张三'
T2 读到的 '李四' 是没提交的"脏数据",这个数据从来没真正存在过

不可重复读(Non-Repeatable Read)

T1:第一次读 name = '张三'
T2:修改 name = '李四' 并提交
T1:第二次读 name = '李四'   ← 不可重复读!
同一事务内两次读同一行,结果不同

幻读(Phantom Read)

T1:SELECT * FROM user WHERE age > 20 → 5 行
T2:INSERT INTO user(age) VALUES(25) → 插入并提交
T1:SELECT * FROM user WHERE age > 20 → 6 行   ← 幻读!
"像幻觉一样多了一行数据"

快速区分:脏读是读了别人没提交的数据;不可重复读是同一行数据被人修改了;幻读是有人插入/删除了数据导致结果集行数变化。

2.2 四种隔离级别

— 查看当前级别
SELECT @@transaction_isolation;
— 设置级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

隔离级别脏读不可重复读幻读实现方式
READ UNCOMMITTED ✅ 发生 ✅ 发生 ✅ 发生 几乎没人用
READ COMMITTED ❌ 不会 ✅ 发生 ✅ 发生 每次查询生成新 ReadView
REPEATABLE READ(MySQL 默认) ❌ 不会 ❌ 不会 部分解决 事务开始时生成一个 ReadView + 间隙锁
SERIALIZABLE ❌ 不会 ❌ 不会 ❌ 不会 读写全加锁,完全串行

面试常见追问:为什么 MySQL 默认是 RR 而不是 RC?

历史原因:早期 binlog 只有 STATEMENT 格式(记录 SQL 语句本身),RC 级别下主库和从库执行同一条 SQL 可能得到不同的结果。虽然现在推荐 ROW 格式了,但 RR 已成为事实标准。另外 RR 配合间隙锁能解决幻读,而 RC 做不到。


搞清楚了"并发会出什么问题",接下来的问题是——数据库具体是怎么实现隔离性的?答案就在锁和 MVCC 之中。


三、锁机制:并发控制的"交警"

3.1 锁的分类地图

               ┌── 共享锁(S 锁 / 读锁):允许别人读,不让别人写
      ┌── 行锁 ─┤
      │        └── 排他锁(X 锁 / 写锁):别人既不能读也不能写
      │
      │        ┌── 记录锁(Record Lock):锁住一行
      │        ├── 间隙锁(Gap Lock):锁住行与行之间的间隙
      │        └── 临键锁(Next-Key Lock):记录锁 + 间隙锁
锁 ───┤
      │        ┌── 意向共享锁(IS 锁)
      └── 表锁 ─┼── 意向排他锁(IX 锁)
               └── 自增锁(AUTO-INC)

先从行锁入手——这是 InnoDB 并发能力的核心,也是 Java 开发最常打交道的锁类型。

3.2 共享锁与排他锁:读写博弈

— 共享锁(S 锁):读的时候加,允许别人也读,但不允许别人写
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

— 排他锁(X 锁):写的时候加,别人既不能读也不能写
SELECT * FROM user WHERE id = 1 FOR UPDATE;
— INSERT / UPDATE / DELETE 默认就加 X 锁

锁兼容矩阵(记住这个就懂了所有的锁冲突):

S 锁X 锁
S 锁 ✅ 可以共存 ❌ S 锁和 X 锁冲突
X 锁 ❌ X 锁排斥一切 ❌ X 锁排斥一切

3.3 临键锁(Next-Key Lock):解决幻读的利器

这是面试中最容易被问到的锁概念。在 REPEATABLE READ 隔离级别下,InnoDB 默认使用 临键锁 = 记录锁 + 间隙锁。

— 假设 user 表有 id = 5, 10, 15 三行
— 间隙分布:(-∞, 5), [5, 10), [10, 15), [15, +∞)

SELECT * FROM user WHERE id > 8 AND id < 13 FOR UPDATE;

这条语句会锁住哪些范围?

实际加锁范围: [5, 10) + [10, 15) + {记录 10}
                 ↑ 间隙锁       ↑ 间隙锁    ↑ 记录锁

效果:既不能修改 id=10 的行,也不能在 5~15 之间插入新行

为什么需要间隙锁? 因为如果没有它,虽然你锁住了 id=10 这行,但别的事务可能在你查询的范围中插入一条 id=11 的记录,导致你的"结果集"在你不知情的情况下被改变了——这就是幻读。间隙锁就像在每个行之间拉了一条"警戒线"。

3.4 死锁:并发编程的经典陷阱

// 事务 A
BEGIN;
UPDATE account SET balance = balance – 100 WHERE id = 1; // 锁住 id=1
UPDATE account SET balance = balance + 100 WHERE id = 2; // 等待锁 id=2
COMMIT;

// 事务 B(同时执行!)
BEGIN;
UPDATE account SET balance = balance – 100 WHERE id = 2; // 锁住 id=2
UPDATE account SET balance = balance + 100 WHERE id = 1; // 等待锁 id=1
COMMIT;

// → 死锁!A 等 B 放锁,B 等 A 放锁,循环等待
// InnoDB 会检测到死锁,回滚其中一个事务,报错:
// Deadlock found when trying to get lock; try restarting transaction

避免死锁的三条黄金法则
  • 按相同顺序访问资源:所有事务都按 id 升序操作,锁的获取顺序一致就不会死锁。
  • 缩短事务时间:事务里不要放 RPC 调用、文件 IO 等耗时操作,锁持有时间越短,死锁概率越低。
  • 适当降低隔离级别:RC 级别下间隙锁较少,死锁率也相应降低。

  • 锁解决了"写-写冲突"和"写-读部分冲突",但光靠锁的话,所有的读都要排队等写——性能就崩了。MySQL 是怎么让"读"和"写"可以同时进行的?答案是 MVCC。


    四、MVCC:读写不冲突的秘密

    4.1 一句话理解 MVCC

    MVCC(Multi-Version Concurrency Control,多版本并发控制)的核心思想很简单:读操作不加锁,通过查看数据的旧版本来获取一致性视图。

    就像你修改一个 Word 文档——你正在编辑的内容别人看不到,别人看到的还是旧的版本。你保存之后别人才看到新版。MVCC 在数据库里做了同一件事。

    4.2 隐藏在每一行背后的两个"幽灵"字段

    InnoDB 的每一行数据都有三个你平时看不到的隐藏字段:

    ┌──────────────────────────────────────────────────┐
    │  id  │  name  │  age  │  DB_TRX_ID │ DB_ROLL_PTR │
    ├──────────────────────────────────────────────────┤
    │  1   │  张三   │  25   │    100     │  0x…      │
    └──────────────────────────────────────────────────┘
                                  ↑              ↑
                      最近修改这一行的事务ID    指向 undo log 的指针

    每次更新数据时,旧版本被存入 undo log,通过 DB_ROLL_PTR 指针串成版本链:

    当前行(name='王五', trx=300)
        ↓ DB_ROLL_PTR
    undo log v1(name='李四', trx=200)
        ↓ DB_ROLL_PTR
    undo log v2(name='张三', trx=100)

    4.3 ReadView:事务的"快照相机"

    ReadView 是事务在读取数据时创建的"快照",记录了这个瞬间有哪些事务正在活跃(还没提交)。

    ReadView 属性含义
    m_ids 创建快照时所有"活跃中"的事务 ID
    min_trx_id m_ids 中最小的那个
    max_trx_id 下一个待分配的事务 ID
    creator_trx_id 创建这个 ReadView 的事务自己

    4.4 可见性判断:我该看到哪个版本?

    当一条数据的 trx_id = 150,你的事务看这条数据时,按以下规则判断:

    规则 1: trx_id == creator_trx_id  →  ✅ 自己的修改当然可见
    规则 2: trx_id < min_trx_id       →  ✅ 修改者在快照前就提交了
    规则 3: trx_id >= max_trx_id      →  ❌ 修改者在快照后才开始,不可见
    规则 4: trx_id IN m_ids           →  ❌ 修改者在快照时还没提交,不可见
    规则 5: 以上都不满足               →  ✅ 修改者在快照前已提交,可见

    4.5 RC vs RR:ReadView 的生成时机决定了一切

    这是面试中最能体现深度的问题之一。两种隔离级别的唯一区别就是 ReadView 的生成时机:

    READ COMMITTED(读已提交):
      事务中每次 SELECT → 生成新的 ReadView
      → 能看到别的事务新提交的数据 → 导致"不可重复读"

    REPEATABLE READ(可重复读):
      事务中第一次 SELECT → 生成 ReadView,之后复用
      → 看不到别的事务新提交的数据 → 保证"可重复读"

    有了 MVCC 的理解基础,你再回头看第一节的三种并发问题(脏读、不可重复读、幻读),应该会豁然开朗——它们本质上都是"事务在什么时候、看到了什么版本的数据"的问题。


    本篇回顾

    学完这一篇,你应该能回答:

    • 事务的 ACID 四个特性分别由什么机制保证?
    • 脏读、不可重复读、幻读的区别是什么?
    • MySQL 四种隔离级别各自解决了什么问题?
    • 共享锁和排他锁的兼容矩阵是什么?
    • 临键锁是如何防止幻读的?间隙锁锁住的是什么?
    • 死锁是怎么产生的?如何避免?
    • MVCC 的 ReadView 是如何判断数据可见性的?
    • RC 和 RR 的本质区别是什么?

    这篇内容可能是整个系列中最"烧脑"的。如果你第一遍没完全理解 MVCC 和锁机制——完全正常。建议把第四节(MVCC)读两遍,然后尝试自己画出 ReadView + 版本链的判断过程。一旦通了,整个事务和锁的知识就串起来了。

    下一篇,我们将走出单机范围,探索 MySQL 的日志系统和高可用架构——redo log 和 binlog 为什么需要两阶段提交?主从复制是怎么实现的?分库分表该怎么做?


    【上一篇:Java开发必知必会的MySQL核心知识点(二)-索引探秘:让你的查询快如闪电】

    赞(0)
    未经允许不得转载:171主机测评 » Java开发必知必会的MySQL核心知识点(三)-深入理解:事务、锁与 MVCC
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址