MySQL 数据库死锁及核心机制全解析

在 MySQL 数据库的并发场景中,死锁、事务隔离、锁机制等问题一直是开发和运维人员面临的核心挑战。本文将从事务并发问题出发,逐步深入数据库隔离级别、InnoDB 锁机制、死锁原理,最后详解 MVCC 多版本并发控制,帮助大家全面掌握 MySQL 并发控制的核心知识。

一、事务并发产生的问题

当多个事务同时操作数据库时,若缺乏有效的并发控制,会引发以下四类问题,其中前三者针对数据行,幻读针对数据表。

1. 脏写

定义 :一个事务修改了另一个未提交事务修改过的数据,随后未提交的事务回滚,导致前者的修改丢失。示例 :数据库中某行数据初始值为null。事务 A 将其修改为A(未提交),事务 B 紧接着将其修改为B(未提交)。此时事务 A 发生回滚,数据恢复为null,事务 B 的修改被覆盖丢失。

2. 脏读

定义 :一个事务读取了另一个未提交事务修改过的数据,随后该未提交事务回滚,导致读取到的数据无效。示例 :事务 A 将数据null修改为A(未提交),事务 B 查询到该数据为A。之后事务 A 回滚,数据恢复为null,事务 B 再次查询时得到null,前后不一致导致业务逻辑异常。

3. 不可重复读

定义 :同一事务内多次读取同一主键的数据,因其他事务提交修改,导致每次读取的字段值不一致。示例 :事务 A 首次查询数据为null,事务 B 修改该数据为B并提交,事务 A 再次查询得到B;随后事务 C 修改该数据为C并提交,事务 A 第三次查询得到C,三次结果不同。

4. 幻读

定义 :同一事务内多次查询符合相同条件的数据,因其他事务插入 / 删除数据,导致每次查询的结果条数不一致。示例 :事务 A 查询human表有 10 条数据,事务 B 向该表插入 3 条数据并提交,事务 A 再次查询时得到 13 条数据,出现 "幻影" 数据。

小结:脏写、脏读、不可重复读针对数据行,幻读针对数据表。

二、数据库的隔离级别

为解决事务并发问题,数据库定义了四种隔离级别,不同级别对并发问题的解决程度不同。

1. 读未提交(Read Uncommitted)

  • 特点:所有事务可查看其他未提交事务的执行结果。
  • 并发问题:脏读、脏写、不可重复读、幻读均会出现。
  • 实际应用:几乎不用,安全性极低。

2. 读已提交(Read Committed)

  • 特点:事务只能查看已提交事务的修改结果(多数数据库默认隔离级别,非 MySQL)。
  • 并发问题:避免脏读、脏写,仍存在不可重复读、幻读。

3. 可重复读(Repeated Read)

  • 特点:同一事务多次读取同一数据行时,结果始终一致(MySQL 默认隔离级别)。
  • 并发问题:理论上存在幻读,MySQL 的 InnoDB 引擎通过 MVCC 机制解决。

4. 序列化(Serializable)

  • 特点:最高隔离级别,强制事务串行执行,对读取的数据行加共享锁。
  • 并发问题:完全避免脏读、脏写、不可重复读、幻读。
  • 缺点:大量锁竞争,易导致超时和性能下降。

隔离级别与并发问题对应关系

隔离级别 脏读 不可重复读 幻读
Read Uncommitted
Read Committed
Repeated Read
Serializable

三、InnoDB 锁机制

InnoDB 提供七种锁类型,按兼容性可分为共享 / 排它锁,按粒度可分为表锁和行锁,核心用于解决并发修改冲突。

1. 共享 / 排它锁(行级锁)

按兼容性分类的行级锁,控制数据读写权限:

  • 共享锁(S 锁) :允许事务读数据,阻止其他事务加排他锁。多个事务可同时加 S 锁(读读兼容)。
    • 语法:select ... lock in share mode;
  • 排他锁(X 锁) :允许事务读写数据,阻止其他事务加 S 锁或 X 锁(写写、读写互斥)。
    • 语法:select ... for update;
  • 注意:加 X 锁后,其他事务仍可进行无锁查询(普通 select)。

2. 意向锁(表级锁)

为支持行级锁与表级锁共存而设计的表级锁,用于声明事务的加锁意向:

  • 分类
    • 意向共享锁(IS 锁):事务意向对表中某些行加 S 锁。
    • 意向排它锁(IX 锁):事务意向对表中某些行加 X 锁。
  • 兼容性:IS 锁与 IX 锁兼容,IS 锁与 S 锁兼容,IX 锁与 X 锁冲突(具体兼容关系见文档图示)。

3. 间隙锁(Gap Locks)

  • 定义:当使用范围条件检索数据并加锁时,InnoDB 不仅对符合条件的索引项加锁,还对条件范围内不存在的 "间隙" 加锁。
  • 示例 :表中 id 为 1-101,执行select * from lock_example where id > 100 for update;,InnoDB 会对 id=101 的记录加锁,同时对 id>101 的间隙加锁。
  • 作用:防止幻读,但会阻塞范围内的并发插入,导致锁等待。
  • 优化建议:尽量使用相等条件访问数据,避免范围查询加锁。

4. 记录锁(Record Locks)

  • 定义:对单个索引记录加锁,仅锁定指定行。
  • 使用条件
    • 查询字段必须是主键或唯一索引。
    • 查询条件为精准匹配(=),不能是 >、<、like 等(否则退化为临键锁)。
  • 示例select * from lock_example where id = 1 for update; 锁定 id=1 的记录。

5. 临键锁(Next-Key Locks)

  • 定义:结合间隙锁与记录锁的锁机制,锁定左开右闭的区间,仅作用于非唯一索引列(唯一索引列会降级为记录锁)。
  • 示例 :表中 age 为非唯一索引,执行update lock_example set name = 'Vladimir' where age = 24;,会锁定 (10,24] 区间,同时锁定下一个区间的间隙,最终锁定 (10,32]。

6. 插入意向锁(Insert Intention Locks)

  • 定义:间隙锁的一种,专门针对 insert 操作,支持同一区间内非冲突插入的并发执行。
  • 示例:事务 A 在 age=10 和 24 之间插入 age=23 的记录,事务 B 可同时插入 age=24 的记录(位置不冲突),不会相互阻塞。

7. 自增锁(Auto-inc Locks)

  • 定义:表级锁,针对 AUTO_INCREMENT 列的插入操作,确保主键值连续。
  • 特点:一个事务插入时,其他事务的插入需等待,直到当前事务提交。

锁机制总结

分类维度 锁类型
兼容性 共享锁(S)、排他锁(X)
粒度 表锁(意向锁、自增锁)、行锁(记录锁、间隙锁、临键锁、插入意向锁)

四、死锁

1. 核心概念

  • 锁等待:事务需操作的资源被其他事务锁定,需等待资源释放(超时则报错)。
  • 阻塞:因锁兼容性,一个事务的锁等待另一个事务的锁释放。
  • 死锁:两个或多个事务相互争夺资源,形成循环等待,若无外力干预则无法推进。

2. 死锁示例

事务 A 修改数据 B 后等待修改数据 A,事务 B 修改数据 A 后等待修改数据 B,双方相互锁定对方所需资源,导致死锁。

3. MySQL 死锁解决方案

  • 超时机制:当等待时间超过阈值,其中一个事务回滚,释放资源。
  • 智能回滚:InnoDB 默认回滚 undo log 量最小的事务(减少性能损耗)。

五、多版本并发控制(MVCC)

MVCC 是 InnoDB 实现非阻塞读的核心机制,通过多版本数据实现读写不冲突,提升并发性能。

1. 核心概念

  • MVCC 定义:多版本并发控制,通过维护数据的多个版本,允许读操作访问历史版本,避免加锁阻塞。
  • 当前读:读取数据最新版本,加锁保证一致性(如 select for update、update、insert、delete)。
  • 快照读:读取数据历史版本,不加锁(普通 select),仅在非串行隔离级别生效。
  • MVCC 优势
    • 读写不阻塞,提升并发性能。
    • 解决脏读、不可重复读、幻读(无法解决更新丢失)。

2. 实现原理

MVCC 依赖隐式字段undo 日志Read View三大组件实现。

(1)隐式字段

每行数据除自定义字段外,包含三个隐式字段:

  • DB_TRX_ID(6 字节):最近修改 / 插入该记录的事务 ID。
  • DB_ROLL_PTR(7 字节):回滚指针,指向 undo 日志中的上一版本数据。
  • DB_ROW_ID(6 字节):隐含自增主键(无主键时自动生成)。
  • 隐藏删除标识:记录删除 / 更新时仅标记,不实际删除。
(2)undo 日志

记录数据的历史版本,分为两类:

  • 插入 undo log:insert 操作产生,事务提交后可立即删除。
  • 更新 undo log:update/delete 操作产生,供快照读和事务回滚使用,由 purge 线程清理。
  • 数据版本链:多次修改后,undo 日志形成链表,通过 DB_ROLL_PTR 串联。
(3)Read View

事务快照读时生成的读视图,用于判断数据版本的可见性,包含三个属性:

  • trx_list:当前活跃事务 ID 列表。
  • up_limit_id:活跃事务最小 ID。
  • low_limit_id:下一个未分配的事务 ID(最大事务 ID+1)。
(4)可见性判断规则
  1. 若数据的 DB_TRX_ID < up_limit_id:数据在当前事务之前提交,可见。
  2. 若数据的 DB_TRX_ID >= low_limit_id:数据在当前事务之后生成,不可见。
  3. 若 DB_TRX_ID 在 trx_list 中:事务未提交,数据不可见。
  4. 不可见时,通过 DB_ROLL_PTR 遍历 undo 日志,直到找到可见版本。

3. 执行流程示例

  1. 事务 1 插入数据(DB_TRX_ID=null,DB_ROLL_PTR=null)。
  2. 事务 1 修改数据,将旧数据写入 undo 日志,更新 DB_TRX_ID=1,DB_ROLL_PTR 指向 undo 日志。
  3. 事务 2 修改同一数据,将当前数据写入 undo 日志,更新 DB_TRX_ID=2,DB_ROLL_PTR 指向事务 1 的版本。
  4. 事务 3 快照读时,生成 Read View,通过可见性规则遍历版本链,找到符合条件的历史版本。

总结

MySQL 并发控制的核心是平衡一致性与性能:

  • 事务隔离级别提供基础一致性保障,InnoDB 默认可重复读通过 MVCC 解决幻读。
  • 锁机制解决写冲突,细粒度行锁提升并发,表锁保证全局一致性。
  • MVCC 通过多版本数据实现非阻塞读,是高并发场景的关键优化。
  • 死锁需通过合理的事务设计(如统一加锁顺序)和 MySQL 的自动回滚机制规避。
相关推荐
Kapaseker2 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴2 小时前
Android17 为什么重写 MessageQueue
android
倔强的石头_18 小时前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android