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
2
3
4
5
6
7
8
9
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别
SET transaction_isolation = 'READ-UNCOMMITTED';
SET transaction_isolation = 'READ-COMMITTED';
SET transaction_isolation = 'REPEATABLE-READ';
SET transaction_isolation = 'SERIALIZABLE';

锁机制

并发访问场景分析

  • 读-读 : 允许并发,无需加锁
  • 写-写 : 任何隔离级别都不允许,必须互斥
  • 读-写/写-读 : 通过锁机制实现互相阻塞

锁的类型

共享锁 (S锁)

  • 用途 : 事务读取记录时获取
  • 特性 : 多个事务可同时持有同一记录的S锁
1
2
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;

独占锁 (X锁)

  • 用途 : 事务修改记录时获取
  • 特性 : 与任何锁都不兼容
1
2
SELECT * FROM account WHERE id = 1 FOR UPDATE;

锁兼容性矩阵

X锁 S锁
X锁 不兼容 不兼容
S锁 不兼容 兼容

锁的粒度

全局锁

对整个数据库实例加锁,主要用于数据备份。

1
2
FLUSH TABLES WITH READ LOCK;

表级锁

1
2
3
4
5
6
7
-- 表级共享锁
LOCK TABLES account READ;
-- 表级独占锁
LOCK TABLES account WRITE;
-- 释放锁
UNLOCK TABLES;

意向锁

  • 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
2
3
4
5
6
7
8
9
IS                    -- 表级意向共享锁
IX -- 表级意向独占锁
S,REC_NOT_GAP -- 行共享锁
X,REC_NOT_GAP -- 行独占锁
S Next-Key Lock -- 行+间隙的共享锁
S GAP -- 间隙共享锁
X,GAP,INSERT_INTENTION -- 等待Gap Lock释放的插入意向锁
X,INSERT_INTENTION -- 等待Next-Key Lock释放的插入意向锁

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
for(current_version in version_chain){
if(curr_version.trx_id == creator_trx_id){
// 当前事务访问自己修改过的版本
// 版本对当前事务可见
return curr_version;
} else if(curr_version.trx_id >= max_trx_id){
// 在当前事务当前查询时,产生该版本的事务,还未提交
// 版本对当前事务不可见
continue;
} else if(curr_version.trx_id < min_trx_id){
// 在当前事务当前查询时,产生该版本的事务,已经提交
// 版本对当前事务可见
return curr_version;
} else if(min_trx_id <= curr_version.trx_id < max_trx_id){
if(current_version.trx_id in m_ids){
// 在当前事务当前查询时,产生该版本的事务还未提交
// 版本对当前事务不可见
continue;
} else {
// 在当前事务当前查询时,产生该版本的事务已经提交
// 版本对当前事务可见
return current_version;
}
}
return null;
}

对于Repeatable Read的MVCC处理

对于Repeatable Read隔离级别来说,它的要求是 对于读操作,在事务存在期间,读信息需要保持一致,这样它会在首次执行时,获取阅读视图,此时,它拥有的阅读视图字段状态是:

  1. m_ids - 在它创建时的活跃事务id
  2. min_ids - 在它创建时的活跃事务id的最小值,这意味着它可以判断哪些是已经提交的行信息
  3. max_ids - 数据库会分配的下一个事务id值,这意味着它可以通过此判断之后的行更新提交
  4. creator_id - 它自身的事务id

这样,当进行查询时:

  1. 当查询到的变更是由它自身进行修改的,那么直接返回该版本,这样在当前事务处理过程中可以看到自己对信息的修改。
  2. 当查询到的变更是由在它之后创建的事务id修改的,那么继续查询更早的版本,因为它之后的事务进行的行修改,不应该被它看到。
  3. 当查询到的变更是由它所知更早的事务id修改的,那么返回该版本,因为它是确定的已经被提交的行信息版本。
  4. 当查询到的变更是由它记录的活跃事务id提交的,且在最大和最小提交范围内,继续查询更早的版本。
  5. 当查询到的变更是由它记录的活跃事务id提交的,不在最大和最小提交范围内,则返回该版本。但是对于当前隔离级别来说,这样是不可能的,因为它拿到的是在它创建时记录的活跃事务ID视图,那么在它创建时,被提交的记录的事务id必然比它更小,这样如果不在最大/最小提交范围内,说明要么已经是提交状态,已经在步骤3中进行了处理,要么是在它之后创建的,已经在步骤2中进行了处理。

对于Read Committed的MVCC处理

对于Read Committed的隔离级别来说,它的要求是 在事务存在期间,可以”看”到其他事务提交的信息,这样在每次尝试查询时,都会获取当前的所有已提交事务之后的行信息,这样,它拥有的阅读视图字段状态是:

  1. m_ids - 当前活跃的所有事务id,这意味着在查询时的这些事务并未被提交
  2. min_ids - 查询时活跃事务id的最小值,这意味着它可以判断哪些事务仍然未被提交
  3. max_ids - 查询时,下一个会被分配的ID,这意味着在它查询时哪些事务仍然未创建
  4. creator_id - 它自身的事务id

这样,当进行查询时:

  1. 当查询到的变更是由它自身进行修改的,那么直接返回该版本,这样在当前事务处理过程中可以看到自己对信息的修改。
  2. 因为获取的是当前的视图信息,所以事务要么处于已经被提交状态,要么处于活跃/创建阶段,并没有数据进行提交。
  3. 当查询到的变更是由它所知更早的事务id修改的,那么返回该版本。
  4. 当查询到的变更是由它记录的活跃事务id提交的,且在最大和最小提交范围内,继续查询更早的版本,因为它所记录的m_ids中都是当前视图下的活跃事务,并没有完成commit,所以不会获取对应的版本。
  5. 当查询到的变更是由它记录的活跃事务id提交的,不在最大和最小提交范围内,则返回该版本。在RC的隔离级别中,因为获取的是当前的阅读视图,这意味着这个事务已经是提交状态,因为max_ids限制了最新的事务范围,而又通过min_ids对更早提交的事务进行了处理,所以在当前阶段获取到的版本是处于当前事务范围段内非最早事务,已经处理并提交了的事务,所以可以返回该版本,满足RC的要求。

总结

数据库隔离级别通过锁机制和MVCC技术在数据一致性和并发性能之间取得平衡。MVCC通过版本链和一致性视图的精妙设计,让不同隔离级别能够以最小的性能代价实现相应的一致性保证。理解这些机制的工作原理,有助于在实际应用中选择合适的隔离级别和优化数据库性能。


MySQL 并发控制
http://gadoid.io/2025/09/05/MySQL-并发控制/
作者
Codfish
发布于
2025年9月5日
许可协议