MySQL 并发控制
本文最后更新于 2025年9月5日 凌晨
并发控制
数据库隔离级别概述
四种隔离级别
- READ UNCOMMITTED (读未提交)
- READ COMMITTED (读已提交)
- REPEATABLE READ (可重复读)
- SERIALIZABLE (串行化)
并发问题类型
脏读 (Dirty Read)
一个事务读取到了另一个未提交事务修改过的数据。这意味着当前事务可能读取到最终会被回滚的数据,导致数据不一致。
不可重复读 (Non-Repeatable Read)
在同一个事务内多次读取同一数据,前后两次读取的结果不一致。这是因为其他事务在此期间提交了对该数据的修改。
幻读 (Phantom Read)
在同一个事务内多次执行相同的查询条件,前后两次查询返回的记录数量不一致。这通常发生在其他事务插入或删除了符合查询条件的记录。
隔离级别详解
读未提交 (READ UNCOMMITTED)
- 特点 : 事务的变更在未提交时就能被其他事务看到
- 问题 : 会出现脏读、不可重复读、幻读
- 性能 : 最高,但一致性最差
读已提交 (READ COMMITTED)
- 特点 : 只能读取已提交事务的变更
- 问题 : 避免了脏读,但仍可能出现不可重复读、幻读
- 应用 : 大多数数据库的默认级别
可重复读 (REPEATABLE READ)
- 特点 : 事务执行期间看到的数据始终与事务启动时一致
- 问题 : 避免了脏读和不可重复读,但在某些情况下仍可能出现幻读
- 应用 : MySQL InnoDB的默认级别
串行化 (SERIALIZABLE)
- 特点 : 通过读写锁强制事务串行执行
- 问题 : 完全避免所有并发问题
- 性能 : 最低,但一致性最强
隔离级别设置
1 | |
锁机制
并发访问场景分析
- 读-读 : 允许并发,无需加锁
- 写-写 : 任何隔离级别都不允许,必须互斥
- 读-写/写-读 : 通过锁机制实现互相阻塞
锁的类型
共享锁 (S锁)
- 用途 : 事务读取记录时获取
- 特性 : 多个事务可同时持有同一记录的S锁
1 | |
独占锁 (X锁)
- 用途 : 事务修改记录时获取
- 特性 : 与任何锁都不兼容
1 | |
锁兼容性矩阵
| X锁 | S锁 | |
|---|---|---|
| X锁 | 不兼容 | 不兼容 |
| S锁 | 不兼容 | 兼容 |
锁的粒度
全局锁
对整个数据库实例加锁,主要用于数据备份。
1 | |
表级锁
1 | |
意向锁
- IS锁 : 意向共享锁,表示事务准备在表中的某行加S锁
- IX锁 : 意向独占锁,表示事务准备在表中的某行加X锁
- 作用 : 提高锁检查效率,避免逐行扫描
元数据锁 (MDL)
- 自动加锁 : 访问表时自动添加
- MDL读锁 : 增删改查操作时添加
- MDL写锁 : 表结构变更时添加
行级锁
InnoDB的行级锁根据隔离级别自动管理:
非串行化级别 :
- 查询操作: 不加S锁,通过MVCC实现
- 修改操作: 加X锁和IX意向锁
串行化级别 :
- 查询操作: 加S锁和IS意向锁
- 修改操作: 加X锁和IX意向锁
Next-Key Lock 和 Gap Lock
Gap Lock (间隙锁)
锁定记录之间的间隙,防止幻读。
Next-Key Lock
锁定记录本身及其前面的间隙。
触发条件对照表
| 锁类型 | 触发条件 | 典型场景 |
|---|---|---|
| Gap Lock | 查询不存在的记录 | WHERE id = 不存在值 |
| Gap Lock | 范围查询的空白区间 | WHERE id BETWEEN 6 AND 9(区间内无记录) |
| Next-Key Lock | 范围查询涉及存在记录 | WHERE id <= 10 |
| Next-Key Lock | 非唯一索引查询 | WHERE name = 'John' |
| Next-Key Lock | 无索引条件查询 | WHERE description = 'test' |
锁模式标识
1 | |
MVCC多版本并发控制
版本链
对于收到的数据更新请求,InnoDB会对每一次变更,生成对应的undo log并将log存储到更新后行信息中,这样在每次更新后,新的行信息中都记录了每一次的数据变更。
一致性视图
在事务获取对应版本时,通过一致性视图中的检索策略来完成对对应版本的获取。
一致性视图中包括以下关键字段:
- m_ids : 当前系统中活跃的读写事务的事务id列表
- min_trx_id : 当前系统中活跃的读写事务中最小的事务id,也就是m_ids的最小值
- max_trx_id : 系统应该分配给下一个事务的事务id值
- creator_trx_id : 当前事务的事务id
一致性视图的原理是:通过m_ids,管理自己所能”看”到的数据版本。从这些数据版本中通过事务ID选择合适的版本进行查询。
1 | |
对于Repeatable Read的MVCC处理
对于Repeatable Read隔离级别来说,它的要求是 对于读操作,在事务存在期间,读信息需要保持一致,这样它会在首次执行时,获取阅读视图,此时,它拥有的阅读视图字段状态是:
- m_ids - 在它创建时的活跃事务id
- min_ids - 在它创建时的活跃事务id的最小值,这意味着它可以判断哪些是已经提交的行信息
- max_ids - 数据库会分配的下一个事务id值,这意味着它可以通过此判断之后的行更新提交
- creator_id - 它自身的事务id
这样,当进行查询时:
- 当查询到的变更是由它自身进行修改的,那么直接返回该版本,这样在当前事务处理过程中可以看到自己对信息的修改。
- 当查询到的变更是由在它之后创建的事务id修改的,那么继续查询更早的版本,因为它之后的事务进行的行修改,不应该被它看到。
- 当查询到的变更是由它所知更早的事务id修改的,那么返回该版本,因为它是确定的已经被提交的行信息版本。
- 当查询到的变更是由它记录的活跃事务id提交的,且在最大和最小提交范围内,继续查询更早的版本。
- 当查询到的变更是由它记录的活跃事务id提交的,不在最大和最小提交范围内,则返回该版本。但是对于当前隔离级别来说,这样是不可能的,因为它拿到的是在它创建时记录的活跃事务ID视图,那么在它创建时,被提交的记录的事务id必然比它更小,这样如果不在最大/最小提交范围内,说明要么已经是提交状态,已经在步骤3中进行了处理,要么是在它之后创建的,已经在步骤2中进行了处理。
对于Read Committed的MVCC处理
对于Read Committed的隔离级别来说,它的要求是 在事务存在期间,可以”看”到其他事务提交的信息,这样在每次尝试查询时,都会获取当前的所有已提交事务之后的行信息,这样,它拥有的阅读视图字段状态是:
- m_ids - 当前活跃的所有事务id,这意味着在查询时的这些事务并未被提交
- min_ids - 查询时活跃事务id的最小值,这意味着它可以判断哪些事务仍然未被提交
- max_ids - 查询时,下一个会被分配的ID,这意味着在它查询时哪些事务仍然未创建
- creator_id - 它自身的事务id
这样,当进行查询时:
- 当查询到的变更是由它自身进行修改的,那么直接返回该版本,这样在当前事务处理过程中可以看到自己对信息的修改。
- 因为获取的是当前的视图信息,所以事务要么处于已经被提交状态,要么处于活跃/创建阶段,并没有数据进行提交。
- 当查询到的变更是由它所知更早的事务id修改的,那么返回该版本。
- 当查询到的变更是由它记录的活跃事务id提交的,且在最大和最小提交范围内,继续查询更早的版本,因为它所记录的m_ids中都是当前视图下的活跃事务,并没有完成commit,所以不会获取对应的版本。
- 当查询到的变更是由它记录的活跃事务id提交的,不在最大和最小提交范围内,则返回该版本。在RC的隔离级别中,因为获取的是当前的阅读视图,这意味着这个事务已经是提交状态,因为max_ids限制了最新的事务范围,而又通过min_ids对更早提交的事务进行了处理,所以在当前阶段获取到的版本是处于当前事务范围段内非最早事务,已经处理并提交了的事务,所以可以返回该版本,满足RC的要求。
总结
数据库隔离级别通过锁机制和MVCC技术在数据一致性和并发性能之间取得平衡。MVCC通过版本链和一致性视图的精妙设计,让不同隔离级别能够以最小的性能代价实现相应的一致性保证。理解这些机制的工作原理,有助于在实际应用中选择合适的隔离级别和优化数据库性能。