MySQL InnoDB 保障并发更新安全核心机制(底层原理)
MySQL 只有 InnoDB 引擎支持事务与行级锁,MyISAM 是表锁,并发差;并发更新安全主要依靠:事务隔离级别、MVCC、行锁(Record‑Lock/Gap‑Lock/Next‑Key‑Lock)、undo‑log、redo‑log、锁等待、死锁检测、事务原子性协同实现。
一、核心概念
- 并发更新安全要解决 2 个问题
- 问题 1:脏写:事务 A 修改一行,事务 B 覆盖 A 未提交的数据(最严重)。
- 问题 2:脏读、不可重复读、幻读。 InnoDB 靠锁解决写‑写冲突;MVCC 解决读写冲突(读不加锁)。
- InnoDB 默认隔离级别:RR(可重复读),MySQL‑RR 级别通过 Next‑Key‑Lock 彻底解决幻读。
一、写‑写并发:靠 InnoDB 行锁体系阻止脏写(核心)
场景:多个事务同时更新同一行数据,例如
update t set num = num‑1 where id = 1;。
1. 锁的粒度
- 如果命中索引:行锁(Record Lock),只锁住匹配行,其他行可正常更新,并发高;
- 如果没有索引:InnoDB 会退化成表锁,并发完全失效,这是开发大忌。
2. RR 级别下 3 种锁(底层核心)
- Record Lock(记录锁):锁定索引上的某一条实际记录
- 只锁住命中的索引行;其他行不受影响。
- Gap Lock(间隙锁):锁住索引记录之间的空隙
- 禁止其他事务在间隙插入新数据,用来防止幻读;
- 仅在 RR 隔离级别生效;RC 级别没有间隙锁。
- Next‑Key Lock(临键锁)= Record‑Lock + Gap‑Lock InnoDB RR 默认使用 Next‑Key‑Lock,左开右闭区间。
- 例:索引有 1,5,10;执行
where id>5 and id<10;锁住区间(5,10],既不能修改 id=10,也不能插入 6‑9 之间数据,杜绝幻读。
- 例:索引有 1,5,10;执行
上锁规则(底层执行逻辑)
- 更新语句执行时,InnoDB 会对扫描到的索引条目加 排他锁(X 锁);
- 其他事务尝试获取该行 X 锁时,会进入锁等待状态;
- 事务提交之后锁才释放,保证同一时刻只能一个事务修改该行,彻底避免脏写。
锁等待与死锁处理
- 锁等待:事务拿不到锁默认等待 50 秒(
innodb_lock_wait_timeout),超时抛出锁等待超时异常; - 死锁:InnoDB 底层有死锁检测线程,主动选择代价更小的事务回滚,打破死锁,不需要人工干预。
二、读写并发:MVCC(多版本并发控制)实现读不加锁
如果读取也加锁,并发会极差。InnoDB 通过 MVCC 做到:写操作加行锁,普通 select 快照读不加锁。
1. MVCC 依赖的底层物理文件:undo‑log(回滚日志)
- 每更新一行数据,旧版本数据放到 undo‑log;
- 每行隐藏字段(非常关键):
DB_TRX_ID:最后修改这行的事务 ID;DB_ROLL_PTR:指针指向 undo‑log 里历史版本;DB_ROW_ID:隐藏主键,如果没有主键就自动生成。
- undo‑log 里多个历史版本通过 roll‑ptr 串联形成版本链。
2. Read View(读视图),控制事务能看到哪些版本
Read‑View 包含:
- m_ids:当前活跃事务集合;
- up_limit_id:最小活跃事务 id;
- low_limit_id:下一个将要分配的事务 ID。
RR 与 RC 的区别(核心)
- RC(读已提交):每次 select 都会生成新 Read‑View,会出现不可重复读,没有间隙锁;
- RR(可重复读):事务第一次 select 创建 Read‑View,之后全程复用同一个 Read‑View,避免不可重复读;配合临键锁解决幻读。
快照读(普通 select):走 MVCC 读取 undo 里历史版本,不加锁;
当前读(select ... for update /update/delete):走行锁读取最新数据。
举例:
- T1 开启事务更新 id=1,加 X 锁;
- T2 普通 select 查询 id=1,不去读取正在修改的未提交数据,而是读取 undo‑log 旧版本,实现读写并行;
- T2 如果执行
select ... for update当前读,则会被阻塞等待行锁释放。
三、事务原子性、持久性:redo‑log + undo‑log 保障更新安全
就算并发期间服务器宕机,更新也不会错乱:
- undo‑log:保证事务回滚 更新前先把旧数据写入 undo‑log,事务失败就依靠 undo‑log 回滚修改;保证事务原子性。
- redo‑log:崩溃恢复 InnoDB 修改数据时先修改内存页(Buffer‑Pool),不会立刻刷磁盘;修改内容先写入 redo‑log(WAL 预写日志);
- 如果宕机,重启后通过 redo‑log 把内存中没刷盘的数据恢复到磁盘,保证已提交的数据持久化;
- redo‑log 是顺序写入,性能很高。
更新简化流程:先写 undo‑log → 写 redo‑log(prepare 阶段)→ 修改内存数据 → 提交事务,redo‑log 置 commit 标记。这就是著名的两阶段提交。
四、完整并发更新流程
- 执行 update 语句,InnoDB 通过索引找到对应记录;
- 给索引记录加 Next‑Key‑Lock(X 排他锁);
- 将修改前的数据写入 undo‑log,构建版本链;
- 修改 Buffer‑Pool 里的数据页;
- 将本次修改记录写入 redo‑log prepare 阶段;
- 事务提交:redo‑log 写入 commit 标记;事务结束释放行锁;
- 其他事务更新同一行:拿不到 X 锁进入等待;普通 select 读取 undo 历史版本;select for update 等待锁释放;
- 服务器宕机:依靠 redo‑log 恢复已提交修改,undo‑log 回滚未提交修改。
五、开发层面避坑
- 更新条件必须命中索引,否则行锁退化成表锁,并发直接报废;
- 尽量缩小事务执行时间,事务越长持有锁时间越长,并发越低;
- RC 隔离级别无间隙锁,可以减少锁范围,但会存在不可重复读;RR 级别杜绝幻读但是锁范围更大;
- 更新顺序统一(例如总是先更新 id 小的)减少死锁产生概率。
总结
MySQL‑InnoDB 依靠排他行锁解决写‑写竞争防止脏写;
MVCC 基于 undo‑log 实现快照读解决读写冲突;
redo‑log 保证宕机后数据持久化,
undo‑log 保证事务回滚;
RR 隔离级别下 Next‑Key‑Lock 临键锁解决幻读,以此保障并发更新安全。