一、三种并发问题对比

问题类型 定义 发生场景 示例 解决隔离级别
脏读 读取到其他事务未提交的数据 READ UNCOMMITTED 事务B读取事务A未提交的修改,事务A回滚 READ COMMITTED
不可重复读 同一事务内多次读取同一数据结果不同 READ COMMITTED 事务A读取数据后,事务B修改并提交,事务A再次读取结果不同 REPEATABLE READ
幻读 同一事务内多次查询同一范围返回不同行数 REPEATABLE READ 事务A查询范围数据后,事务B插入新数据并提交,事务A再次查询发现”幻影行” SERIALIZABLE

二、详细说明与示例

1. 脏读 (Dirty Read)

定义:一个事务读取了另一个未提交事务修改过的数据。

发生条件

  • 隔离级别为READ UNCOMMITTED
  • 一个事务修改数据但未提交
  • 另一个事务读取了这个未提交的数据

示例

1
2
3
4
5
6
7
8
9
10
11
12
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 余额从1000改为900
-- 尚未提交

-- 事务B (READ UNCOMMITTED隔离级别)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 读取到900(脏数据)
-- 如果此时事务A回滚,事务B读取的就是无效数据

-- 事务A
ROLLBACK; -- 余额恢复为1000,但事务B已经使用了错误的900这个值

危害

  • 基于脏数据做出错误决策
  • 数据一致性被破坏

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

定义:同一事务内,多次读取同一数据返回不同结果(因为其他事务已提交修改)。

发生条件

  • 隔离级别为READ COMMITTED或更低
  • 一个事务读取数据后
  • 另一个事务修改该数据并提交
  • 第一个事务再次读取同一数据

示例

1
2
3
4
5
6
7
8
9
10
11
12
-- 事务A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 返回1000

-- 事务B
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 改为900
COMMIT;

-- 事务A
SELECT balance FROM accounts WHERE id = 1; -- 返回900(与第一次读取不同)
COMMIT;

与脏读的区别

  • 脏读读取的是未提交数据
  • 不可重复读读取的是已提交的修改

危害

  • 同一事务内数据不一致
  • 可能导致业务逻辑错误

3. 幻读 (Phantom Read)

定义:同一事务内,多次执行同一查询返回不同行数的结果(因为其他事务已提交新增/删除数据)。

发生条件

  • 隔离级别为REPEATABLE READ或更低
  • 一个事务读取某个范围数据后
  • 另一个事务在该范围插入/删除数据并提交
  • 第一个事务再次读取同一范围

示例

1
2
3
4
5
6
7
8
9
10
11
12
-- 事务A
START TRANSACTION;
SELECT * FROM products WHERE price > 10; -- 返回2条记录

-- 事务B
START TRANSACTION;
INSERT INTO products VALUES (3, 'New Product', 15);
COMMIT;

-- 事务A
SELECT * FROM products WHERE price > 10; -- 返回3条记录(出现了"幻影行")
COMMIT;

与不可重复读的区别

  • 不可重复读针对的是已存在的行的值变化
  • 幻读针对的是行数的变化(新增或删除的行)

三、MySQL各隔离级别对问题的解决

隔离级别 脏读 不可重复读 幻读 实现机制
READ UNCOMMITTED ❌ 可能 ❌ 可能 ❌ 可能 无锁
READ COMMITTED ✅ 解决 ❌ 可能 ❌ 可能 行锁(写时)
REPEATABLE READ ✅ 解决 ✅ 解决 ⚠️ 部分解决* MVCC+间隙锁
SERIALIZABLE ✅ 解决 ✅ 解决 ✅ 解决 完全串行化

*注:InnoDB在REPEATABLE READ级别通过间隙锁(Gap Lock)解决了大部分幻读问题,但某些特殊场景仍可能出现

四、实际应用建议

  1. 默认选择REPEATABLE READ
  • MySQL的默认级别
  • 平衡了性能和数据一致性
  • InnoDB的优化使其幻读问题很少出现
  1. 需要更高性能时考虑READ COMMITTED
  • 适用于对一致性要求不高的场景
  • 比REPEATABLE READ并发性能更好
  1. 关键业务使用SERIALIZABLE
  • 金融、支付等绝对要求一致性的系统
  • 注意性能代价和死锁风险
  1. 避免使用READ UNCOMMITTED
  • 除非是只读统计等特殊场景
  • 数据不一致风险太大

MySQL 并发控制机制详解:行锁、MVCC 和间隙锁

MySQL 的 InnoDB 存储引擎通过多种机制实现并发控制,主要包括行级锁、多版本并发控制(MVCC)和间隙锁。这些机制共同工作,在保证数据一致性的同时提供良好的并发性能。

一、行锁 (Row-Level Locking)

基本概念

行锁是锁定索引记录(而非整张表)的锁机制,InnoDB 支持两种行锁:

  1. **共享锁(S锁)**:
  • 又称读锁
  • SELECT ... LOCK IN SHARE MODE 显式获取
  • 多个事务可以同时持有同一行的共享锁
  1. **排他锁(X锁)**:
  • 又称写锁
  • SELECT ... FOR UPDATEUPDATEDELETE 等操作自动获取
  • 一旦有事务持有某行的排他锁,其他事务不能获取该行的任何锁

行锁特点

  • 锁定粒度小,并发度高
  • 只锁定需要操作的记录,不影响其他记录
  • 死锁概率比表锁高,但并发性能更好

行锁示例

1
2
3
4
5
6
7
8
-- 事务1
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 获取id=1的排他锁
-- 其他事务不能修改id=1的记录

-- 事务2 (并发执行)
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 会被阻塞,等待事务1释放锁

二、多版本并发控制 (MVCC - Multi-Version Concurrency Control)

基本概念

MVCC 通过保存数据在某个时间点的快照来实现非锁定读,是 InnoDB 实现 READ COMMITTED 和 REPEATABLE READ 隔离级别的关键机制。

MVCC 核心组件

  1. 隐藏字段
  • DB_TRX_ID:记录最近修改该行的事务ID
  • DB_ROLL_PTR:回滚指针,指向undo日志
  • DB_ROW_ID:隐藏的自增行ID
  1. ReadView
  • 事务执行SQL时产生的一致性读视图
  • 包含:活跃事务ID列表、最小活跃事务ID、下一个事务ID等
  1. undo日志
  • 记录数据被修改前的值
  • 用于事务回滚和实现MVCC

MVCC 工作原理

  1. SELECT 操作
  • 只查找版本早于当前事务的数据
  • 确保读取的是事务开始时的数据快照
  1. INSERT/DELETE/UPDATE 操作
  • 创建新版本数据
  • 旧版本存入undo日志

MVCC 示例

1
2
3
4
5
6
7
8
9
10
11
-- 事务1 (事务ID=100)
START TRANSACTION;
SELECT * FROM products WHERE id = 1; -- 读取当前快照

-- 事务2 (事务ID=101)
START TRANSACTION;
UPDATE products SET price = 20 WHERE id = 1;
COMMIT;

-- 事务1
SELECT * FROM products WHERE id = 1; -- REPEATABLE READ下仍读取旧值,READ COMMITTED下读取新值

三、间隙锁 (Gap Lock)

基本概念

间隙锁锁定索引记录之间的”间隙”,防止其他事务在范围内插入新记录,解决幻读问题。

间隙锁特点

  • 只存在于REPEATABLE READ和SERIALIZABLE隔离级别
  • 锁定的是索引记录之间的区间
  • 可以防止幻读,但会降低并发性能

间隙锁类型

  1. **记录锁(Record Lock)**:锁定单个索引记录
  2. **间隙锁(Gap Lock)**:锁定索引记录之间的区间
  3. **临键锁(Next-Key Lock)**:记录锁+间隙锁的组合

间隙锁示例

1
2
3
4
5
6
7
-- 表中有id为10,20,30的记录
START TRANSACTION;
SELECT * FROM users WHERE id > 15 AND id < 25 FOR UPDATE;
-- 会锁定(10,20)和(20,30)这两个区间,防止插入id=16,17,...24等记录

-- 其他事务尝试插入
INSERT INTO users VALUES(18, 'name'); -- 会被阻塞

四、三种机制的协同工作

  1. 写操作
  • 使用行锁(排他锁)锁定要修改的记录
  • 必要时添加间隙锁防止幻读
  • 创建新版本数据并记录undo日志
  1. 读操作
  • 普通SELECT使用MVCC实现非锁定读
  • 锁定读(SELECT FOR UPDATE)使用行锁
  • 范围查询在REPEATABLE READ下使用间隙锁
  1. 事务隔离级别影响
  • READ UNCOMMITTED:不使用MVCC,可能脏读
  • READ COMMITTED:MVCC每次读创建新ReadView
  • REPEATABLE READ:MVCC使用事务开始时的ReadView,加间隙锁
  • SERIALIZABLE:所有SELECT转为SELECT FOR SHARE,使用更多锁

五、实际应用建议

  1. 合理设计索引
  • 行锁和间隙锁都依赖索引
  • 无合适索引会导致锁表
  1. 控制事务大小
  • 大事务持有锁时间长,影响并发
  • 将大事务拆分为小事务
  1. 避免不必要的锁定读
  • 只在必要时使用SELECT FOR UPDATE
  • 优先使用普通SELECT+应用层校验
  1. 监控锁争用
  • 使用SHOW ENGINE INNODB STATUS查看锁情况
  • 关注innodb_row_lock_waits等状态变量

理解这些并发控制机制有助于优化数据库性能,设计更高效的应用程序。

Copy-on-Write 是一种优化策略,只有在真正需要修改数据时才会创建副本,否则多个使用者共享同一份数据。