一条UPDATE到底加多少锁?InnoDB锁机制全面拆解

发布时间:2026/8/29 4:31:38
一条UPDATE到底加多少锁?InnoDB锁机制全面拆解 “一条UPDATE到底加了多少锁”这个问题我被人问过也问过别人。第一次被问到的时候我脑子里只有一个“行锁”的概念支支吾吾说了几句就冷场了。后来翻了官方文档、看了源码解析、也亲手在测试库里反复验证才发现这根本不是“一条语句”的问题而是“一串知识点”的连锁反应。这篇把整套东西整理出来既是给自己做个沉淀也希望能帮正在准备面试或者想彻底搞懂InnoDB锁的读者少走点弯路。先说结论一条UPDATE加多少锁取决于三个维度——隔离级别、WHERE条件有没有走索引、走的是什么类型的索引。这三个维度组合起来答案从“一把锁”到“锁表”都有可能。更关键的是面试官追问的往往不是那个数字而是你判断的过程。所以这篇文章不光讲答案更会手把手拆解怎么推导答案以及怎么看MySQL系统表去验证锁的真实情况。1. 思路拆解面对“UPDATE加锁”面试题的正确打开方式1.1 为什么面试官偏偏选中UPDATE来问锁说句实话面试官拿UPDATE来问锁是性价比极高的考察方式。一条UPDATE语句背后牵扯了事务隔离级别、索引结构、锁的粒度、MVCC机制、甚至可能发生的死锁场景。你如果能把一条UPDATE的加锁过程完整讲清楚等于把InnoDB层面并发控制的核心逻辑都覆盖了一大半。而且UPDATE本身是日常开发里最高频的写操作不像某些冷门函数那样脱离实际。面试官问这个问题就是想知道你在实际开发中写出的每一条UPDATE底层到底发生了什么。平时写SQL不关心执行计划的人遇到这种题很容易翻车。我遇到过很多候选人对“SELECT加不加锁”能说上两句但一到UPDATE就开始含糊。说到底UPDATE和SELECT最大的区别是UPDATE在InnoDB里属于“当前读”加的全是排他锁而普通SELECT是快照读靠MVCC保证一致性根本不加锁。这个底层差异决定了UPDATE天然就是锁问题的重灾区。1.2 回答这个问题前必须先建立的三个坐标系把这题吃透核心是头脑里要有一套分析框架。我自己总结成三个坐标系按顺序过一遍答案自然就出来了。第一隔离级别坐标系。MySQL默认的RR级别下InnoDB为了防幻读引入了间隙锁和临键锁。而在RC级别下间隙锁基本退场加锁范围会小很多。所以同一句UPDATE在RC和RR下锁的数量可能差好几倍这是第一个要明确的前提。第二索引类型坐标系。WHERE条件走的是主键索引、唯一索引、普通索引还是压根没走索引对应的加锁策略完全不同。这个坐标系又包含两个子问题一是“走了什么索引”二是“等值还是范围”。等值匹配和范围匹配加锁范围不是一个量级。第三命中情况坐标系。WHERE条件筛选出来的数据“有没有真实存在”直接影响锁的实际行为。等值查询如果完全没命中记录在RR级别下会在符合条件的区间加一把间隙锁而不是加记录锁。很多面试者忽略这个细节一说就是“加N把锁”完全没考虑命中与否的差异。有了这三个坐标系再去看具体SQL你就不是背答案而是做推导。接下来的内容就是围绕这三个坐标系逐个展开的。2. 底层视角InnoDB锁的分类逻辑和加锁机制2.1 行锁的三种形态记录锁、间隙锁、临键锁想搞懂UPDATE加锁得先搞清楚InnoDB里行锁的三种基础形态。第一种是记录锁最简单也最好理解就是锁定索引上的某一条具体记录也就是对应某一行的数据。第二种是间隙锁它锁的不是某条记录而是两条记录之间的“空隙”目的就是防止其他事务在这个区间插入新数据从而避免幻读。第三种是临键锁你可以把它理解为“记录锁间隙锁”的组合体它同时锁住一条记录以及它前面的那个间隙区间。这里有个新手很容易踩的误区很多人以为记录锁锁的是“行数据”实际上InnoDB的锁是加在索引上的。如果一个表没有定义主键InnoDB会隐式创建一个主键作为聚簇索引锁最终还是落在索引项上。理解了这一点后面看到“为什么普通索引查询还会锁主键索引上的记录”就不会懵了。补充一个小知识点间隙锁和临键锁只在RR级别及以上的隔离级别才有效。在RC级别下InnoDB为了减少锁竞争只保留记录锁这也是RC并发性能通常更好的原因之一。所以面试时如果你听到对方说“RR级别下大量间隙锁导致性能下降”你就知道他确实理解这个机制。2.2 加锁的顺序和两种特殊锁意向锁与插入意向锁除了行级锁InnoDB还有表级意向锁。这个设计很多人不太理解其实它就是一道“门卫登记”事务准备给某一行加锁之前必须先给表加上意向锁IS或IX向其他事务声明“我打算在这个表里加锁了”。这样别的事务想给整张表加锁时看到意向锁就能立刻意识到“里面已经有人持锁”不用一行行去扫描判断。意向锁本身不阻塞任何事务它存在的意义只是快速判断表级锁能否获取。插入意向锁也是容易被忽略的概念。它本质上是一种特殊的间隙锁但它不是阻塞其他插入而是多个事务在同一个间隙的不同位置插入时插入意向锁之间是互相兼容的。打个比方间隙锁就像一个停车场的空车位范围插入意向锁就是每个来停车的车在自己车位上放的标志两辆车只要不在同一个车位就可以同时停进来。很多人会问理解这些跟“UPDATE加了多少锁”有什么关系关系很大。当你去系统表查询锁信息时会看到一条UPDATE相关的锁记录里可能有多个锁类型字段REC_NOT_GAP记录锁、GAP间隙锁、以及INSERT_INTENTION插入意向锁等。只有能准确区分这些类型才算真正读懂了锁的真实情况。2.3 加锁的基本单位为什么索引是核心再看底层索引结构InnoDB的B树叶子节点是按索引列的大小顺序排列的双向链表。正是因为这个有序结构锁才能精准定位到“某一条记录”或者“某个区间”。记录锁定位到的是某个叶子节点对应的聚簇索引项间隙锁定位的是叶子节点链表上两个相邻项之间的空档。面试中常见的进阶问法是“UPDATE where id 7这行不存在会加锁吗”答案是在RR级别下会。因为InnoDB会在扫描过程中以某个已存在的临近记录作为锚点把该相邻区间加上间隙锁防止其他事务插入id7这条记录。这个机制给很多人造成了迷惑明明UPDATE一行都没更新却锁了一个区间其他事务的INSERT还可能被阻塞。当你把所有锁都理解为“加在索引项和索引项之间的空隙”之后上面的行为就顺理成章了。这也是为什么索引列的选择性如此重要你建的索引越精准InnoDB扫描的索引项越少锁的范围就越小并发能力自然就越高。3. 加锁清单推导不同条件下UPDATE到底加多少把锁3.1 主键等值且命中最简单的情况就是一把记录锁先看最干净利落的场景执行UPDATE user SET name 张三 WHERE id 10且id10这条记录确定存在隔离级别是RR。此时InnoDB会怎么做第一步根据主键id10在聚簇索引B树上查找这条记录第二步找到之后在聚簇索引叶子节点对应的索引项上加一把X型记录锁。所以这种情况下的答案就是“一把锁”——但注意是一把加在聚簇索引上的排他记录锁。如果这条记录恰好还被某个二级索引覆盖到那么二级索引会不会加锁这里有个经典八股误区很多人以为主键查询只锁聚簇索引就够了。实际上如果UPDATE语句修改了二级索引列的值比如执行UPDATE user SET age 20 WHERE id 10而age列上有二级索引那InnoDB还必须维护二级索引对age对应的二级索引项也加锁。所以严格说修改哪个索引列哪个索引项就得一并上锁。所以回答“主键等值”时最严谨的说法是先判断UPDATE语句修改了哪些索引列然后分别对聚簇索引项和涉及的二级索引项加锁如果不修改任何二级索引列那就是主键索引上一把记录锁。3.2 主键等值未命中RR级别下加的是间隙锁继续看主键等值但id10这条记录不存在表里只有id8和id15两条记录。这时候RR级别下会怎么样InnoDB在扫描聚簇索引时发现没有id10的记录但它会记录下扫描过程中遇到的第一个大于10的索引项也就是id15这一项。此时会在(8,15)这个开区间上加一把间隙锁。这个间隙锁不是锁住id15这行数据而是锁住“8和15之间可以插入新记录”的能力。那么这条UPDATE到底加了几把锁答案是一把间隙锁对应范围(8,15)。有人会纠结这个间隙锁算不算“锁”当然算。它的作用非常明显锁住这个区间防止其他事务往这里插入id9、id10等记录。但如果你在RC级别下执行同样语句什么锁都不会加——因为RC没有间隙锁机制。这里插一个重要提示UPDATE没找到记录时也会产生锁等待。遇到过实际案例SQL里更新一条已经物理删除的数据结果很多事务并发执行都停在“Updating”状态排查之后才知道是间隙锁互相等待导致的。3.3 唯一索引等值二级索引和聚簇索引各一把记录锁假设user表有个唯一索引uk_email执行UPDATE user SET name 李四 WHERE email testexample.com。这条SQL怎么加锁拆解一下查找过程。InnoDB先通过Email这棵唯一索引B树定位到对应二级索引项在这个二级索引项上加一把X记录锁。接着它会通过该二级索引项上保存的主键值假设是id20回表去聚簇索引查对应主键记录再在聚簇索引项上加一把X记录锁。所以这个场景的标准答案是“两把记录锁”一把在uk_email二级索引项上一把在id20的聚簇索引项上。这两把锁必须同时持有因为只有同时锁住二级索引项和聚簇索引项才能防止其他事务通过其他索引路径再次访问到这条记录。很多人只答出“两把锁”就停了其实还可以补充一个细节如果这个UPDATE语句还修改了email字段本身比如SET email newexample.com那原email对应的二级索引项和新的email对应的二级索引项都要处理锁的情况会更复杂。删除旧索引项再插入新索引项的过程中还可能需要获取插入意向锁。3.4 普通索引等值记录锁间隙锁锁的是一个区间普通索引和唯一索引的本质区别是“不唯一”。即使你查询的是一个等值条件也可能匹配到多条记录。比如user表的age列上有普通索引现在执行UPDATE user SET name 王五 WHERE age 25。由于age25的记录可能存在多条InnoDB的加锁策略会更加保守。它会从第一个匹配的age25索引项开始向右扫描到第一个age不属于25的索引项为止。在这个过程中所有age25的二级索引项都加X记录锁同时这些索引项之间的间隙也要加间隙锁。关键点来了普通索引等值的UPDATE加的是“临键锁”的集合——不只是匹配行本身还包括匹配行前后的间隙。这样做的目的是防止别的会话在区间内插入新的age25记录避免当前UPDATE前后两次扫描结果不一致。所以这种场景加锁数量是“N个记录锁 (N1)个间隙锁”等值条件下通常表现为临键锁。如果要给个数没人能给出精确数字——因为取决于匹配到几行。但作为面试答案你只要能把推导过程讲出来就行“有多少条匹配记录就加多少个二级索引记录锁同时锁住相关间隙另外每个匹配记录回表的主键记录也都要加锁。”3.5 范围查询锁定范围扩大锁数量随之增加范围查询UPDATE user SET status 1 WHERE id 100在RR级别下加的锁就更多了。InnoDB会沿着聚簇索引一直向右扫描每扫到一条id100的记录就加一把X记录锁同时在其前面的间隙加间隙锁直到扫到一条不满足条件的记录停下来最后一共加了大量临键锁。如果这个范围非常大比如id1那么基本等于锁住了全表绝大多数记录。这正是很多开发者在生产环境执行批量UPDATE导致线上大面积阻塞的原因。这里需要特别提醒UPDATE ... WHERE id 100和UPDATE ... WHERE id 100引起的锁范围并不是简单的一线之差。前者不会锁id100这条记录后者会锁。而所有范围查询在RR级别下除了记录锁之外还会在边界前后加间隙锁以封锁新记录的插入。所以回答此类问题一定要分情况讨论边界条件。3.6 无索引或索引失效最危险的全表加锁如果WHERE条件没有走任何索引比如UPDATE user SET name 赵六 WHERE gender male且gender列上没有索引那InnoDB只能做全表扫描。全表扫描意味着什么它会从聚簇索引的第一条记录开始逐条扫描到最后一条每一条都尝试加X锁。注意别以为它会“智能地只锁匹配到的记录”它实际上是扫描过程中把每一条扫描过的记录都加上锁。最终表现就是这张表的所有记录几乎都被加了X锁等于把整张表锁住了其他事务的INSERT/UPDATE/DELETE全部阻塞。你可能会问为什么不加索引就会锁全表因为InnoDB只有在索引上才能精准定位行锁的锚点没有索引就等于无法判断哪条记录会被修改。为了在事务期间保证数据一致性它只能把扫描路径上的所有记录全部锁起来。这时候的锁数量严重依赖表行数十万行表就是十万把锁性能可想而知。所以面试时被问到这条答案的落点是没有索引会全表加锁不但锁数量极多还极易引发锁等待和死锁。解决方法是给WHERE条件加一个合适的索引让InnoDB有精准加锁的入口。4. 实操验证用性能字典表亲眼看看UPDATE加了几把锁4.1 搭建验证环境懂了理论还是得眼见为实。我习惯用MySQL 8.0的performance_schema.data_locks表来查看锁信息它比5.7时代的information_schema.innodb_locks更全面能看到每条持锁事务对应的锁模式、锁类型、索引名称和锁定的记录信息。验证前的准备很简单建一张测试表插入几条数据然后开两个会话。一个会话执行UPDATE后先不提交另一个会话查询锁信息。核心操作是START TRANSACTION后执行UPDATE再开一个新终端执行SELECT * FROM performance_schema.data_locks\G就能看到当前未提交事务持有的全部锁。我在本地库建了一张user表字段包括id主键、name普通字段、email唯一索引、age普通索引。插入了id从1到6的6条数据age分别为23、25、25、26、27、28。专门把age25放了两条方便验证普通索引等值命中的多行场景。4.2 主键等值命中的验证结果第一个实验UPDATE user SET nametest WHERE id3。事务不提交查data_locks表结果非常干净只有一行锁记录LOCK_MODE是X,REC_NOT_GAP说明是一把X型记录锁加在聚簇索引PRIMARY上锁定记录就是id3的索引项。这个实验验证了理论上“主键等值命中 一把记录锁”的判断。需要注意观察点是LOCK_MODE中的REC_NOT_GAP标记它表示锁定的只是记录本身不涉及间隙。第二个实验执行UPDATE user SET nametest WHERE id100id100不存在。此时data_locks表显示一条锁记录但LOCK_MODE变成了X,GAP说明锁定的是一个间隙。再看LOCK_DATA字段显示的是15也就是第一个大于100的索引项值。这说明锁定的间隙在(6,15)之间与前面理论推导完全吻合。4.3 普通索引等值的验证结果第三个实验UPDATE user SET nametest WHERE age25注意age有两个25的记录。执行后查锁信息发现锁记录明显变多。二级索引idx_age上有一组锁包括age25对应的多个索引项记录锁X,REC_NOT_GAP以及这些索引项之间的间隙锁X,GAP同时聚簇索引上id2、id3对应的记录也各有一把记录锁。这个结果直观展示了普通索引等值UPDATE的加锁规模有多少个匹配的二级索引项就会有多少个二级索引记录锁每个匹配项回表的主键索引也各有一把锁二级索引匹配项之间的间隙也被锁死。实际数下来一把UPDATE产生的锁记录轻松超过5条。执行完这些实验建议顺手清掉事务ROLLBACK再进入下一个场景避免持有锁干扰后面的实验。4.4 实战中学到的锁排查技巧通过这套验证方法最高频的价值体现在排查“线上业务卡顿”时。比如说业务反馈某个更新操作偶尔等不到行锁直接用SELECT * FROM performance_schema.data_locks;查一下就能看到事务A持有谁的锁事务B在等谁的锁。有时候还能看到事务C同时持有A需要的某个锁和B需要的某个锁构成死锁循环。另外一个实用技巧是结合performance_schema.data_lock_waits表来看等待关系。它记录了哪个事务在等哪个事务持有的哪把锁配合sys.innodb_lock_waits视图可以拿到等待时间、阻塞源头SQL等信息定位起来非常快。这几个表在8.0里是排查锁问题的主入口建议所有后端和DBA都熟练掌握。平时看太多理论分析真到线上排查时会发现表里的信息远比经验推断更准确。5. 面试官后续追问如何不慌不忙延伸开去5.1 从UPDATE引申出的死锁场景面试官问完加锁数量大概率紧跟一个死锁问题。最经典的就是两个事务互相更新对方已锁定的记录例如事务A更新id1再更新id2事务B更新id2再更新id1两个事务各持有一把锁同时等对方释放另一把锁死锁形成。讲死锁时建议把UPDATE加锁机制串起来一条UPDATE持有多把锁而多把锁的获取顺序决定了死锁是否会发生。如果两个事务都以同样的顺序先id1后id2更新就不会死锁只有顺序交叉才会死锁。咱们平时写批量更新时按固定顺序排序再执行就是为了避免交叉加锁引发的死锁。MySQL处理死锁的方式是死锁检测机制检测到循环等待后会主动回滚代价较小的事务并抛出一个Deadlock found when trying to get lock; try restarting transaction错误。面试时可以补充一句InnoDB的死锁检测默认开启如果高并发下大量事务争锁检测本身也会消耗性能这属于另一个深入话题了。5.2 并发控制思路乐观锁和悲观锁讲完InnoDB的行锁面试官很可能顺势问“那你们业务里用乐观锁还是悲观锁”。这里要区分清楚InnoDB的行锁是悲观锁的典型实现它在操作前就默认会有冲突所以预先加锁来保证安全。乐观锁则不一样它默认冲突很少操作时不加锁提交时通过版本号或CAS机制检测冲突。遇到这类问题最稳的回答方式是结合实际需求并发冲突不严重、读多写少的场景建议乐观锁比如更新用户资料失败重试即可库存扣减、金额变动这类高频写且不容超卖的场景悲观锁或带条件的原子更新更稳妥。平时业务里经常用UPDATE ... WHERE version ?这种条件更新其实本质就是乐观锁的一种落地方式。补充一句更新库存时UPDATE stock SET num num - 1 WHERE id ? AND num 0不是乐观锁而是原子操作这类条件更新放在UPDATE加锁的主题下同样是最佳实践既利用了行锁保证原子性又通过条件判断防止超卖完全不需要额外引入版本号机制。5.3 数据库锁之外Redis分布式锁的适用边界如果面试聊得深还可能延伸到“为什么有些场景要用Redis分布式锁而不是数据库锁”。数据库锁用在单库事务内没问题但在微服务多实例架构下多个服务操作的是同一个数据库连接池里的不同连接数据库行锁仍然有效但如果目标是跨多个数据库、甚至跨多个中间件资源协调时数据库行锁就管不过来了。Redis分布式锁比如常见的SET lock_key unique_value NX EX 30实现核心优势是性能高、和数据库解耦、适合高频临界区。劣势是无法像数据库事务那样回滚数据需要靠锁超时和业务幂等兜底。面试时能说出数据库锁和Redis锁各自的适用边界是一种很强的加分表现。每个方案都有代价我在实际项目里通常会在单表单记录变更时优先考虑数据库自带的行锁一旦涉及多个服务并发操作共享资源且需要跨事务边界才会去引入分布式锁组件。这个实践经验比理论背得再熟都有说服力。5.4 一条UPDATE引发的索引优化建议面试最后很可能落到优化上“既然普通索引和无索引加锁范围这么大你们线上怎么避免”这个话题我最有感悟因为踩过坑。最重要的建议是UPDATE和DELETE语句的WHERE条件必须走索引最好走主键或唯一索引。如果业务确实只能按普通字段筛选也要保证这个字段上有合适的索引尽量把匹配行数控制在一个很小的范围。之前我们线上有个表按订单状态做批量更新status字段没有索引每次更新都导致全表锁后来给status加了二级索引锁冲突立刻降了一个数量级。其次是控制事务粒度。一条UPDATE不要扫太多行大批量数据更新要分批做每批几百条事务尽早提交让锁快速释放。这在数据订正场景尤其重要。很多线上锁等待其实不是单条UPDATE有问题而是一个人把几百万行的UPDATE放在一个事务里导致锁迟迟不释放。最后建议开发环境开启死锁日志和慢查询日志把innodb_print_all_deadlocks设置为ON这样每次死锁都会把完整加锁细节打印到错误日志里。有日志才有下一步优化的依据。6. 高频追问模板面试官在这题上挂人的三个细节6.1 RC级别下的UPDATE锁是不是真的少了很多很多候选人讲完RR级别下的间隙锁后面试官会追问一句“那RC下呢”这个问题的坑在于有人会脱口而出“RC下UPDATE只加记录锁”这不够准确。RC级别下InnoDB确实不会加间隙锁和临键锁加锁范围会显著缩小。但要注意UPDATE执行时依然要先走索引定位给匹配到的记录加X记录锁再回表加聚簇索引记录锁。如果WHERE条件没走索引RC下同样面临全表扫描加锁的问题。所以标准回答是RC的锁范围确实比RR小但也别以为无索引的UPDATE在RC下就安全了。任何隔离级别下无索引UPDATE的代价都不低只是RC不会加额外的间隙锁而已。6.2 锁和MVCC是不是矛盾有人会把锁和MVCC对立起来理解这也是一个容易聊崩的点。严格说MVCC是InnoDB实现高并发读的核心机制用快照实现读写不阻塞而当前读包括UPDATE则是通过加锁来实现顺序化写。二者不是非此即彼而是分工明确普通SELECT走MVCC快照读不加锁UPDATE/DELETE/SELECT...FOR UPDATE走当前读加锁。面试时可以补一句RC下的MVCC每次生成新的快照RR下只在事务第一次读时生成快照所以RR能实现可重复读。而为了防止“当前读”下的幻读RR才需要间隙锁。这个解释把MVCC、隔离级别、锁全部串起来了会让面试官觉得你成体系地理解了MySQL。6.3 “一条UPDATE锁了多少行”能答出精确数字吗这是面试官最爱设的陷阱你前面分析得头头是道他最后突然问“那到底锁了几行”。这时候不要慌也别硬编数字直接说这依赖实际数据分布和执行计划。严谨的表述是锁定的索引项数等于二级索引匹配项数 聚簇索引回表项数 间隙锁相关索引项数。如果不涉及二级索引的主键等值查询那通常就是1个聚簇索引项普通索引等值查询数量取决于匹配记录数无索引则是全表索引项数。让面试官感受到你不是在背答案而是有真实排查问题的思路这比一个标准答案重要得多。这个回答方式本身就是从实际排障经验里提炼出来的。我在线上排查问题时从来不是靠猜“锁了几行”而是直接查data_locks表看清楚真实的锁分布。当你把系统表当工具用起来时你得到的锁信息远比任何“理论答案”都可靠。