MySQL的锁机制是个老生常谈但能把行锁、间隙锁、临键锁这三兄弟一次讲透的内容还真不多。尤其是准备面试的朋友背了一堆八股文被问到InnoDB在可重复读隔离级别下到底怎么加锁就露馅线上的同学遇到锁等待超时、死锁只知道重启事务碰运气。这篇文章我打算用两个终端实测的方式把行锁Record Lock、间隙锁Gap Lock、临键锁Next-Key Lock从原理到复现掰开揉碎再附上锁冲突和死锁的排查思路。无论你是后端开发、DBA还是正在刷面试题照着敲一遍基本就不会再被锁糊弄了。1. 先搞清楚InnoDB为什么要设计这么多锁1.1 锁到底在解决什么问题数据库的锁本质是在解决并发控制问题。同一时刻多个事务去读写同一行数据如果没有约束数据就会变得不可预测。比如两个事务同时把某条订单的金额从100改成200和300最终结果可能是200也可能是300谁后提交谁覆盖这种更新丢失是业务绝对不能接受的。InnoDB靠三样东西协同工作MVCC多版本并发控制处理快照读锁机制处理当前读和写写互斥事务日志undo log、redo log保证崩溃恢复。锁在这个体系里的定位是写写互斥和防止幻读。也就是说普通的SELECT快照读通常不碰锁但SELECT ... FOR UPDATE、UPDATE、DELETE这些当前读操作必须拿到对应记录的锁才能继续。面试里最常问的隔离级别和锁的关系是这样的读未提交太松会有脏读读已提交RC下InnoDB只加行锁解决了脏读但挡不住幻读可重复读RR作为MySQL默认隔离级别靠间隙锁和临键锁把幻读也一并压住。换句话说RR之所以能安心做默认值就是因为有间隙锁和临键锁在背后兜底。1.2 三种行级锁的分类与关系很多人以为行锁、间隙锁、临键锁是三个并列的东西这其实是一个误区。严格来说它们是一个族系记录锁、间隙锁、以及记录锁和间隙锁组合出来的临键锁。先看官方文档里对InnoDB行级锁的定义Record Lock记录锁锁定的是索引记录本身。Gap Lock间隙锁锁定的是索引记录之间的空隙覆盖记录之间的范围以及记录之前的范围。Next-Key Lock临键锁本质是Record Lock Gap Lock的组合锁定的范围是左开右闭区间例如(5, 10]。有一个关键背景InnoDB是索引组织表所有数据都挂在索引的B树上所以行锁字面上锁的是行物理上锁的其实是索引记录。这也解释了为什么走不上索引的行锁会升级成全表扫描锁——不是锁表是扫描过每一条索引记录都加锁效果等同于锁了全表。这是面试和排障经常踩的坑后面实操部分我会专门演示。平时说的临键锁在RR隔离级别下是默认的加锁单位。当查询条件命中某条索引记录时InnoDB会选择该记录及其之前的区间加临键锁形成(前一条记录的值, 当前记录的值]这种左开右闭区间。等值查询命中唯一索引时退化成记录锁等值查询没命中记录时退化成纯间隙锁范围查询则会锁住扫描路径上的多个区间。1.3 为什么锁要加在索引上理解这一点很多关于锁的问题都能自己推导出来。InnoDB的二级索引叶子节点存的是主键值主键索引叶子节点存的是完整行数据。无论是加记录锁、间隙锁还是临键锁最终都要定位到具体的索引记录。因为SQL是通过WHERE条件驱动执行的MySQL先根据条件从B树找到候选记录再决定对哪些索引记录加锁。举个例子WHERE amount 200如果amount上有普通索引InnoDB沿着idx_amount找到值为200的叶子节点然后在主键索引上找到对应的数据行。加锁时两个索引上的相关记录都要处理二级索引记录加锁回表后主键索引记录也要加锁。如果amount没有索引优化器只能全表扫描每扫描一行就加一个锁行锁数量等于表行数性能自然雪崩。2. 行锁、间隙锁、临键锁各自的底层逻辑2.1 行锁锁住的是一条实实在在的索引记录记录锁是最好理解的——它锁一条索引记录其他事务不能修改、删除这条记录当前事务则可以继续持有锁做后续操作。行锁分为共享锁S锁和排他锁X锁S锁和S锁兼容S锁和X锁互斥X锁和X锁互斥。只有唯一索引或主键索引上的等值查询且命中了存在的记录时临键锁才会退化成单纯的记录锁。为什么因为索引唯一MySQL可以确认满足条件的记录只有这一条没有间隙需要保护。比如WHERE id 3id是主键这条SQL最终只锁主键索引上id3那一条记录前后的间隙完全不锁所以并发插入id2或id4不会被阻塞。这是行锁最高效、并发度最好的场景也是做高并发订单系统时希望尽量达到的状态。2.2 间隙锁锁住的是记录之间那块空地间隙锁锁的不是某条记录而是两个索引记录之间的开区间。比如表里有id1、3、5三条记录间隙锁可以锁(1,3)、(3,5)也可以锁(0,1)这种前置间隙还可以锁(5, 正无穷)。间隙存在的意义只有一个防止其他事务在间隙里插入新记录。这个防止插入的动作正是RR下避免幻读的机制。事务A在RR下执行范围查询发现id4的记录不存在回到RC就算你重新执行一遍同样的查询还是查不到id4。但如果事务B能在此期间插入id4并提交事务A再查询时就会多出一条之前没见过的记录这就是幻读。间隙锁把(3,5)这个区间锁住事务B的插入会因为想要在锁定的间隙里插入而阻塞幻读自然被堵死了。注意一个反直觉的点间隙锁之间是互相兼容的。两个间隙锁可以同时加在同一个区间上因为防止别人插入这个动作不会冲突它俩的诉求一致。但间隙锁和插入意向锁冲突——插入意向锁是等待间隙释放的信号。所以实操中经常出现两个事务都加了间隙锁又在等待对方的插入意向锁导致死锁的复杂局面。2.3 临键锁Record Lock加Gap Lock的混合体临键锁把记录锁和间隙锁合到一起锁的范围类似(1,3]即把3这条记录本身锁住同时把1和3之间的间隙锁住。为什么InnoDB需要这种混合物因为MVCC和当前读在RR下有一个矛盾要防止幻读既要挡住别人插入新记录间隙锁又要保证自己正在读的记录不被改掉记录锁。拆开来看是两种锁合起来就是一个临键锁。举一个实操里非常典型的现象amount上有普通索引数据包含amount100、200、300。事务A执行SELECT * FROM user_order WHERE amount 200 FOR UPDATE表面上是等值查询但因为amount不是唯一索引InnoDB不敢断言只有一条200所以扫描时会顺带把200前后的区间都保护起来。实际加的锁是(100,200]临键锁和(200,300]临键锁。后果你想不到——其他事务想插入amount150或250都会被阻塞。即使150和250这两条记录根本不存在临键锁的间隙部分依然把它们挡在门外。这就是普通索引等值查询引发大量阻塞挂起的真正原因。2.4 加锁规则速记根据几十次实操下来的经验我总结了几条可以当尺子用的加锁规则。这些规则能覆盖90%的日常问题等值查询命中唯一索引且记录存在只加记录锁不加间隙锁。等值查询命中唯一索引但记录不存在加间隙锁锁住该记录应该存在的那段区间。等值查询命中普通索引加多个临键锁区间范围取决于扫描命中的上下两条边界记录。范围查询只要数据扫描超过边界就会锁住更大的间隙甚至锁到正无穷。所有锁只在当前读语句上生效普通SELECT走MVCC不会加锁。这些规则建议直接背下来面试问一个SQL加了哪些锁基本就是这些组合的推导。3. 实操复现两个终端把三种锁看个明白3.1 准备测试环境纸上谈兵没意思我建议你在本地MySQL 5.7或8.0上跟我一起敲一遍。先建一张订单表CREATE TABLE user_order ( id INT PRIMARY KEY, order_no VARCHAR(20), amount DECIMAL(10,2), status TINYINT, KEY idx_amount (amount) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO user_order VALUES (1, A001, 100.00, 1), (3, A003, 200.00, 1), (5, A005, 300.00, 1), (7, A007, 400.00, 1), (9, A009, 500.00, 1);确认当前隔离级别确保在RR下操作SELECT transaction_isolation;MySQL 5.7及之前版本字段名是tx_isolation。输出应该是REPEATABLE-READ。然后开两个终端窗口分别连接数据库记作会话A和会话B。所有演示的核心就一句话在会话A开事务加锁但不提交然后去会话B执行SQL观察是否阻塞。动手前先把innodb_lock_wait_timeout调小一点避免每次等待默认的50秒SET SESSION innodb_lock_wait_timeout 10;两个会话都执行这样验证阻塞效果的时候最多等10秒就有结果。3.2 演示行锁唯一索引等值命中只锁一行第一步会话A执行BEGIN; SELECT * FROM user_order WHERE id 3 FOR UPDATE;此时事务A持有id3这条记录上的排他记录锁。第二步在会话B执行UPDATE user_order SET status 2 WHERE id 3;你会看到会话B的SQL卡住直到会话A执行COMMIT或ROLLBACK才会返回。第三步在会话B把条件换成另一条记录UPDATE user_order SET status 2 WHERE id 5;你会发现这条SQL立即返回Rows matched为1。这就把行锁只锁一行的本质演示得很彻底id5的记录没有被会话A的锁覆盖完全不阻塞。值得多说一句会话B里第一条阻塞SQL如果等待超过10秒我们设置的超时时间会直接报错ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction。这就是线上锁等待超时的熟悉配方。3.3 演示间隙锁等值查询没命中记录时锁的是区间回到会话A继续新开一个事务BEGIN; SELECT * FROM user_order WHERE id 4 FOR UPDATE;表里根本没有id4这条记录结果集为空。但你千万别以为没查到东西就不用加锁。在RR隔离级别下InnoDB为了挡住其他事务插入id4会在(3,5)这段间隙上加间隙锁。切到会话B执行插入INSERT INTO user_order VALUES (4, A004, 250.00, 1);这条插入会卡住。原因就是插入意向锁和间隙锁互斥——正好插到已经被锁住的(3,5)区间里。等会话A提交后插入立即成功。再做一个对照实验会话B插入id2会成功吗会成功因为id4这条查询只在(3,5)加了锁(1,3)区间是安全的。这很好地验证了间隙是开区间id4不存在锁住的间隙就是3和5之间的那一段不包含3和5本身也不包含(1,3)。3.4 演示临键锁普通索引等值查询扫一片这是本文最核心的一个实验。新开事务会话ABEGIN; SELECT * FROM user_order WHERE amount 200.00 FOR UPDATE;amount上有普通索引idx_amount等值查询命中amount200这条记录但由于索引不唯一InnoDB会保守加锁。实际加锁范围是(100,200]和(200,300]两个临键锁区间也就是把200这条记录以及它前后的两块间隙全部锁住。现在到会话B执行如下三条SQL看看结果INSERT INTO user_order VALUES (2, A002, 150.00, 1); -- 阻塞落在(100,200) INSERT INTO user_order VALUES (4, A004, 250.00, 1); -- 阻塞落在(200,300) UPDATE user_order SET status 2 WHERE amount 200.00; -- 阻塞200这行本身持有记录锁三条全部阻塞都等着会话A释放锁。这组实验会让你深刻理解临键锁为什么叫临键它把命中的记录本身锁记录和相邻的记录间隙锁空隙一起管住了。业务里经常遇到的明明只更新了一条记录但其他无关插入都堵住了十有八九就是普通索引等值查询触发了多个临键锁。最后在会话B测试插入区间外的数据验证边界INSERT INTO user_order VALUES (2, A002, 350.00, 1);这条不论是否卡住都有可能触发间隙锁边界扫到(300,400]因为InnoDB在普通索引上做等值查询时会继续向后扫描到第一个不满足条件的记录也就是amount300那条并把300之前的间隙锁住。这是RR下当前读向后扫描的经典行为。4. 锁冲突与死锁的排查实战4.1 锁等待超时的定位三板斧线上遇到ERROR 1205 Lock wait timeout exceeded第一反应不是重启应用而是先查当前有哪些事务在跑。MySQL 5.7及更早版本看information_schema.innodb_trxSELECT * FROM information_schema.innodb_trx\G重点看trx_state、trx_started、trx_query、trx_mysql_thread_id。一般来说锁等待超时都意味着某个事务长时间不提交把一批行锁或间隙锁牢牢握在手里。把时间最早的那个事务找出来确认是不是业务代码里忘了提交然后用trx_mysql_thread_id对应的连接ID去KILLKILL 123456;注意KILL的是线程ID不是事务ID。KILL之后长事务回滚被阻塞的SQL就能继续跑。如果要看锁的等待关系MySQL 5.7可以用information_schema.innodb_lock_waitsSELECT * FROM information_schema.innodb_lock_waits\GMySQL 8.0把锁信息迁到了performance_schema.data_locks和data_lock_waits查询更直观SELECT * FROM performance_schema.data_lock_waits\G输出里会有等待锁的事务ID、持有锁的事务ID、锁类型和锁对象索引等信息。顺着等待关系找到持有锁的事务再顺着持有锁的事务找到对应的业务SQL问题基本就定位了。4.2 死锁的典型场景和处理办法死锁和锁等待超时不同死锁是事务互相等待对方持有的锁谁也无法推进InnoDB会自动检测并回滚其中一个事务报错码是ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction。我用上面的表实际复现一个经典死锁。会话ABEGIN; UPDATE user_order SET status 2 WHERE id 1;会话BBEGIN; UPDATE user_order SET status 2 WHERE id 5;此时A持有id1的行锁B持有id5的行锁。然后会话A继续UPDATE user_order SET status 2 WHERE id 5;被阻塞因为id5的行锁在B手里。紧接着会话B执行UPDATE user_order SET status 2 WHERE id 1;死锁立刻触发InnoDB死锁检测器介入回滚其中一个事务一般是回滚代价较小的那个。查看死锁细节用SHOW ENGINE INNODB STATUSSHOW ENGINE INNODB STATUS\G输出里的LATEST DETECTED DEADLOCK段落会详细记录两个事务分别持有和等待哪些锁、执行了什么SQL是排查死锁的第一手资料。线上如果死锁频繁多数不是MySQL配置问题而是业务逻辑对幂等记录按不同顺序更新导致的比如行A更新又更新行B和行B更新又更新行A同时发生。解决办法是把多条记录的更新操作在业务层排好序例如统一按主键ID升序更新死锁概率会大幅下降。4.3 线上锁问题的高频根因排过N次锁问题之后我发现真正的根因翻来覆去就那么几个。第一是SQL没走索引导致行锁退化成大面积锁。EXPLAIN看执行计划如果typeALL或者keyNULL那这条SQL只是碰巧没全表扫描锁的数量却一点没省。第二是事务跨度太大查询完了不提交还在继续处理业务逻辑锁被无限期持有。第三是普通索引等值更新本来是点更新结果临键锁扫出一大片区间把无关插入全挡了。第四才是真正的死锁但比例远低于前三种。处理锁问题有一个基本顺序先查长事务再查执行计划是否走了索引然后看锁等待关系最后分析业务更新顺序。按照这个顺序走80%的锁告警都能在半小时内收敛。至于调参innodb_lock_wait_timeout只影响等待时间治标不治本innodb_deadlock_detect在高并发热点更新场景可以关掉换innodb_lock_wait_timeout兜底但绝大多数业务保持默认就好别为了调优制造更多不稳定。在我实际项目里还有一个很深的体会锁问题看起来是数据库的锅本质上经常是业务代码或SQL写法的问题。把最小化锁粒度和最小化事务时长这两个原则贯彻到开发规范里比任何高级配置都管用。最后分享一个小技巧做锁机制实验时永远要用两个数据库会话千万别在同一个会话里自己开事务自己查那样锁的持有方和等待方是同一个人你是观察不到任何阻塞效果的。会话A开事务加锁会话B负责触发阻塞会话A负责释放锁这套流程用熟了锁和事务的表现你都能信手拈来。
企业数字化 ERP 产品动态
相关推荐
Wi-Fi 6的调度机制怎么影响体验?OFDMA与TWT的关键作用 我最近被一个词折腾了挺久——ax调度。起因是办公室一台AP下面,十几个终端同时在线,视频会议、大文件同步、智能家居定时上报一股脑全上来,整体体验直接崩成幻灯片。当时去后台看了一眼,终端全协商在HE档位,速率参数一… · 2026/9/26 6:03:29
MalConv恶意软件检测实战:从ZIP解压到ONNX部署 简介:本资源是一份面向计算机专业本科生的毕业设计与课程设计实践项目,聚焦深度学习在恶意软件检测领域的落地应用,基于经典MalConv模型实现端到端二进制文件分类。资源包共20个文件,包含3个核心Python训练/推理脚本(m… · 2026/9/26 6:03:29
OpenClaw智能体生产落地实践:从最小闭环到渠道接入与避坑指南 简介:《2026厦大团队:智能体OpenClaw(小龙虾)应用实践-94页.pdf》是一份面向AI爱好者与从业者的科普讲座讲义,由厦门大学大数据教学团队整理,系统梳理了从图灵测试、1956年达特茅斯会议到未来AI五个发展阶段… · 2026/9/26 6:03:28
Pytest实战指南:从fixture到参数化与插件扩展全解析 Pytest 是我这几年用得最顺手的 Python 测试框架,没有之一。从刚接触自动化测试时只会写assert断言,到后来用动态参数化把几百条测试数据压进同一个用例,再到自己写钩子扩展框架行为,这条路走下来,我踩过的坑、绕过的弯… · 2026/9/26 6:36:44
VS Code v1.70.3 Windows 7 免安装版实战指南 简介:本资源是专为Windows 7用户定制的Visual Studio Code最终兼容版本(v1.70.3)解压即用包,面向仍需在老旧系统上进行开发、调试或轻量编码的程序员、教育工作者及技术爱好者,解决Win7停更后无法运行新版VSCode的现实… · 2026/9/26 6:36:44
金融系统开发前提:为何必须提供具体技术场景 我无法基于当前输入生成符合要求的博文。原因如下:项目标题为 "financial-services",这是一个高度泛化的行业术语,本身不构成具体可操作、可拆解、可复现的项目;项目正文为空,无任何功能描述、技术实现、业务… · 2026/9/26 6:36:44
给大模型装上“长期记忆”:AI记忆系统设计与落地实践 写AI应用,最头疼的不是模型选型,也不是Prompt调优,而是“记忆”。做过AI助手、聊天机器人、Agent类项目的朋友应该都有体会:模型本身是“记不住事”的,你和它聊十句话,它可能连你第一句说过什么都忘了。我自… · 2026/9/26 6:36:44
ReentrantLock与AQS源码解析:从抢座位到队列机制 抢座位的场景,我估计大家都经历过:上课铃响前,教室前排的好位置就那么几个,来得早的人先坐下,不来的人位置空着;一旦有人离开座位,旁边等的人立刻补上去。Java里的ReentrantLock干的事ÿ… · 2026/9/26 6:36:44
海光K100_AI跑MiniMax-H3视频生成全栈调优指南 1. 项目概述:为什么海光K100_AI单卡跑MiniMax-H3视频生成,必须调优?最近两周,我连续在三台不同配置的国产AI工作站上部署MiniMax-H3模型用于视频帧生成任务,其中两台搭载海光K100_AI加速卡——不是NVIDIA A100或H100&a… · 2026/9/26 6:36:38
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46