1. 为什么我决定亲手验证InnoDB锁机制
1.1 一次线上锁等待超时事故带来的思考
先说说我为什么要写这篇东西。几个月前线上一个订单系统突然频繁报错,错误日志里那句 Lock wait timeout exceeded; try restarting transaction 刷了整整几百屏。当时第一反应是慢查询拖垮了数据库,但翻了一遍慢日志,发现罪魁祸首其实就是几条看起来再普通不过的 UPDATE 语句。等我把事务隔离级别、索引结构、锁范围全部排查清楚之后,发现问题的根源居然是业务代码里一个不起眼的批量更新逻辑,在并发场景下把大量行锁互相叠加,最终把整个表拖入了锁等待的泥潭。
那次事故之后我下了个决心:不能只停留在“会写SQL、会建索引”的程度,必须把InnoDB的锁机制从头到尾亲手验证一遍。MySQL相关知识体系里,索引和锁是最容易“纸上谈兵”的部分,网上的文章一搜一大把,但真正把它们跑起来、观察清楚的人并不多。这篇博客就是我基于一次完整实验得到的记录,内容全部围绕 MySQL InnoDB 锁机制 展开,覆盖了行锁、间隙锁、临键锁、意向锁、表锁以及死锁复现和排查手段,适合正在学习MySQL底层原理的后端开发、即将面试的求职者,以及想搞明白锁等待问题的运维同学。
1.2 锁机制研究的整体思路与实验目标
我先说清楚这篇博客的实验思路,免得你后面看着看着迷路。整个实验分成了几条主线:第一,从锁的粒度出发,验证行级锁和表级锁各自的加锁范围和互斥关系;第二,从锁的兼容性出发,验证共享锁与排他锁在不同事务之间的阻塞表现;第三,单独抽出行级锁的三种实现形态(记录锁、间隙锁、临键锁),用实际SQL去判断它们的边界到底在哪里;第四,复现一个经典死锁场景,并演示如何通过死锁日志和系统表定位问题。
这条路径设计是有讲究的。很多人对锁的理解只停留在“悲观锁、乐观锁”的层面,但InnoDB的锁体系比这复杂得多。单纯开启两个事务互相做 UPDATE,只能证明“行锁存在”,证明不了范围查询为什么能把别人 INSERT 都堵住,也解释不了为什么一个事务只动了一行数据,却会让另一个事务连建表都失败。所以我选择了“由底层到上层”的顺序:先把基础锁类型搞明白,再叠加索引、隔离级别等变量,最后落到实际问题的排查方法上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. InnoDB锁的完整分类体系与设计逻辑
2.1 从粒度维度拆解:行级锁、表级锁、意向锁
InnoDB的锁体系,如果只用一个维度来切分,最直接的就是锁的粒度。粒度大的锁是表级锁,直接锁住整张表,代价低、并发能力差;粒度小的是行级锁,只锁住命中的记录行,并发能力强,但管理和检测的开销更大。InnoDB的默认行为是尽可能使用行级锁,但如果查询条件无法命中索引,优化器可能会退化为全表扫描,行锁的范围就会失控地扩大到整张表,这也是很多“行锁变表锁”事故的来源。
在表级锁和行级锁之间,还有一类特别容易被人忽略的锁,叫意向锁,它本质上是一个“声明”,说“我准备在某个行上加锁了”。拿到意向锁之后,行锁和表锁之间才能快速判断是否有冲突,避免每次加表锁时都要扫描所有行去检查有没有行锁。我后面会专门用实验来演示意向锁的作用,这里先记着这个概念。
2.2 从兼容性维度拆解:共享锁与排他锁
锁的另一个重要维度是兼容性。共享锁(S锁)是读锁,多个事务可以同时持有共享锁读同一行;排他锁(X锁)是写锁,一旦某个事务对某一行加了X锁,其他事务既不能加S锁也不能加X锁,只能排队等待。这套规则和读写锁是同一个思路,在保证数据一致性的同时尽量提高并发度。
不过在InnoDB里有个很有意思的点:普通的 SELECT 查询默认走的是MVCC快照读,根本不需要加锁,只有在显式使用 SELECT ... FOR UPDATE 或 SELECT ... FOR SHARE 时才会走当前读并加上真正的锁。这一点特别容易让初学者误解,以为所有查询都会产生锁。实际上,在高并发多版本控制的机制下,大量读操作并不会互相阻塞。
2.3 行锁的三种实现形态:记录锁、间隙锁、临键锁
行级锁这个概念在InnoDB里并不是单纯的“锁一行”,而是根据锁住的是一个确定的值、还是一个范围区间,分成了三种形态:
- 记录锁(Record Lock):锁住索引上具体的单条记录,主键等值查询命中记录时最典型。
- 间隙锁(Gap Lock):锁住记录之间的间隙,不让其他事务在这个间隙里插入数据,主要解决幻读问题。
- 临键锁(Next-Key Lock):记录锁和间隙锁的组合体,锁住“左开右闭”的索引区间,是InnoDB默认隔离级别下范围扫描的默认锁策略。
这里有个关键点:间隙锁和临键锁只在 RR(REPEATABLE READ) 隔离级别下生效,如果把隔离级别降为 RC(READ COMMITTED),间隙锁会大幅弱化,只保留记录锁。这也是很多系统的线上数据库从RR切到RC来提升并发能力的原因之一,代价是可能再次出现幻读问题。这个取舍后面我也会有实操演示。
3. 实验环境搭建与第一轮验证:行级锁的作用范围
3.1 实验环境准备与测试表设计
先交代一下实验环境。我用的是MySQL 8.0.33,存储引擎确认是InnoDB,事务隔离级别用的默认 REPEATABLE READ。这里提醒一下,MySQL 8.0里 information_schema 的很多锁信息查法跟5.7不一样了,实验时需要用到 performance_schema.data_locks 和 performance_schema.data_lock_waits 两张表来查看锁信息,后面我会专门演示。
测试表的建表语句如下:
sql复制CREATE TABLE `user_info` (
`id` int NOT NULL,
`name` varchar(50) DEFAULT NULL,
`age` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入几条方便观察间隙的数据,注意故意在id上留出空缺:
sql复制INSERT INTO `user_info` VALUES (1, '张三', 18), (3, '李四', 24), (5, '王五', 28), (7, '赵六', 36), (10, '孙七', 42);
选这个数据分布是有讲究的。主键值1、3、5、7、10之间存在多个空隙(比如3到5之间有4),测试间隙锁是否阻塞插入时,用空出来的id就能很直观地判断间隙锁的边界。索引方面同时保留了主键索引和辅助索引 idx_age,方便分别验证不同索引路径下的加锁行为。
3.2 实验一:主键等值查询下记录锁的加锁行为
现在开始第一个实验,先看最基础的记录锁。实验需要开启两个MySQL会话,分别称为事务A和事务B。
事务A执行:
sql复制BEGIN;
SELECT * FROM `user_info` WHERE `id` = 3 FOR UPDATE;
这条语句走主键索引,命中 id=3 的记录,理论上应该在主键索引上加一个排他记录锁。此时我打开第二个会话,事务B尝试更新同一行:
sql复制BEGIN;
UPDATE `user_info` SET `age` = 25 WHERE `id` = 3;
事务B执行后不会立刻返回,而是进入等待状态。这就在MySQL终端里卡住了,一直等到 innodb_lock_wait_timeout(默认50秒)超时。我为了让实验节奏快一点,先把超时时间调成了20秒,也可以在另一个终端实时查询 performance_schema.data_lock_waits 来观察阻塞关系。
如果事务B更新的是其他行呢?比如:
sql复制UPDATE `user_info` SET `age` = 30 WHERE `id` = 5;
这条语句能立刻执行成功。说明事务A的排他锁确实只锁住了 id=3 这一行,不会影响其他记录。这就是记录锁的基本行为:锁住索引上确定的一条记录,范围不扩散。主键等值查询且记录存在时,加的就是这样一个精确的锁,不产生任何间隙锁,所以在事务A未提交的情况下,其他事务可以往相邻id里插入新数据。
这里我顺带测了一个很多人会搞混的场景——事务B在事务A持锁期间对相同行做普通 SELECT:
sql复制SELECT * FROM `user_info` WHERE `id` = 3;
这条语句能秒回,读到的还是旧数据。原因就是前面提到过的MVCC快照读:普通SELECT走的是undo log版本链,不需要等待排他锁释放。这个场景解释了一个常见现象:一个事务在更新某行时,其他事务读这行数据并不会卡住。只有当事务B也打算更新同一行、或者使用 FOR SHARE / FOR UPDATE 做当前读时,才会排队等待。
3.3 实验二:无匹配记录时间隙锁的触发条件
第一个实验验证了“记录存在时加记录锁”的表现。那如果查询条件没有命中任何记录呢?这是间隙锁最容易出现的场景。
事务A执行:
sql复制BEGIN;
SELECT * FROM `user_info` WHERE `id` BETWEEN 4 AND 6 FOR UPDATE;
id=4 和 id=6 在表里都不存在,这条查询返回空结果集。按常理来说,没有返回任何记录,应该不会锁住什么东西。但实际上在RR隔离级别下,InnoDB对这条查询会加间隙锁,锁住 (3,5) 和 (5,7) 这两个区间,也就是把主键索引上4、5、6这些位置全部覆盖。
为了验证这个间隙锁的存在,事务B执行插入:
sql复制INSERT INTO `user_info` (`id`, `name`, `age`) VALUES (4, '测试', 20);
执行后事务B立刻卡住,等待事务A释放锁。这里有个非常容易踩坑的点:间隙锁和记录锁不一样,它锁的不是一个真实存在的记录,而是一个“空的区间”。因此如果事务B尝试插入 id=4 或者 id=6,都会被锁阻塞,但事务B尝试插入 id=8 或 id=9,却能正常执行,因为这些位置不在锁定的间隙范围内。
我再补一个更极端的测试,验证间隙锁对已有记录的更新是否有影响:
sql复制UPDATE `user_info` SET `age` = 30 WHERE `id` = 5;
在事务A持有 (3,5) 和 (5,7) 间隙锁的情况下,事务B更新 id=5 这条已有记录是可以成功的。原因是记录锁和间隙锁是两种独立的锁,事务A的间隙锁只阻止插入,不该限制已有记录的更新。这个行为极易被人误解成“间隙锁会锁住整个区间内的所有操作”,实际上它只拦截 INSERT 操作中落在间隙内的部分。
重要:间隙锁的设计目的是解决幻读问题。它本质上是在说“这个区间现在有主了,任何想往里面插入新数据的操作都给我等着”。理解这一点之后再去看那些“为什么我没更新到记录却把别人插入堵死了”的报障,思路就会清晰得多。
4. 第二轮验证:间隙锁与临键锁的真实影响范围
4.1 实验三:范围查询下的临键锁区间判定
临键锁是记录锁和间隙锁的组合,它锁的是一个“左开右闭”的区间。这个定义比较抽象,我通过一个范围查询来验证。
事务A执行:
sql复制BEGIN;
SELECT * FROM `user_info` WHERE `id` BETWEEN 3 AND 6 FOR UPDATE;
这个查询的返回结果是 id=3 和 id=5 两条记录。加锁范围是怎样的呢?我通过开启事务B做一系列尝试来画出边界:
| 事务B的操作 | 结果 |
|---|---|
UPDATE user_info SET age=30 WHERE id=3 |
阻塞 |
UPDATE user_info SET age=30 WHERE id=5 |
阻塞 |
INSERT INTO user_info VALUES (4, '测试4', 20) |
阻塞 |
INSERT INTO user_info VALUES (6, '测试6', 22) |
阻塞 |
INSERT INTO user_info VALUES (11, '测试11', 50) |
成功 |
UPDATE user_info SET age=30 WHERE id=1 |
成功 |
UPDATE user_info SET age=30 WHERE id=10 |
成功 |
从结果反推可以确定,事务A的锁覆盖了 (1,3]、(3,5]、(5,7] 这几个临键区间。id=3 和 id=5 的记录本身被记录锁锁住,它们前后的间隙(2、4、6这些位置)也被间隙锁锁住,而 id=1 和 id=10 因为是区间外记录所以不受影响。
这里我特意观察到 INSERT INTO user_info VALUES (11, ...) 能执行成功,说明查询范围没有把间隙锁扩展到 (7,10] 之后的区间。所以临键锁的范围严格由索引值和扫描路径决定,不会无限扩大。如果表里数据量很大而查询条件没有合适的索引,扫描路径变成全表,那锁的范围就会是整个表的所有间隙和记录,那基本等于把整张表的写入全部拦住,这就是实战中必须杜绝的灾难性场景。
4.2 实验四:插入意向锁与间隙锁的冲突关系
再来讲一个比较冷门但面试常考的锁类型——插入意向锁(Insert Intention Lock)。当多个事务同时想向同一个间隙插入数据时,InnoDB并不是让它们排队等待,而是先各自生成一个插入意向锁。插入意向锁之间是互相兼容的,因此多个事务可以同时向相同间隙的不同位置插入数据,因为它们插入的目标记录不冲突。
但如果已经有一个事务持有了间隙锁,那么任何插入意向锁都会等待。我把实验场景复现一下:
事务A执行:
sql复制BEGIN;
SELECT * FROM `user_info` WHERE `id` BETWEEN 4 AND 6 FOR UPDATE;
此时事务A持有 (3,5) 和 (5,7) 的间隙锁。然后事务B和事务C几乎同时执行:
sql复制INSERT INTO `user_info` (`id`, `name`, `age`) VALUES (4, '事务B插入', 20);
sql复制INSERT INTO `user_info` (`id`, `name`, `age`) VALUES (6, '事务C插入', 22);
两个事务都会被事务A的间隙锁拦住。这里就要注意了:如果事务A一直不提交,事务B和事务C都会陷入等待,但它们之间并不互相阻塞。我把事务A提交后,再用 performance_schema.data_lock_waits 查看,会发现事务B和事务C的插入请求会同时获得许可,而不是排队一个一个来。这就是插入意向锁“并发插入”特性的实际体现。
这个知识点在实际业务中的启示是:批量插入时如果想避开间隙锁导致的相互阻塞,尽量让各事务插入的主键值不在同一个间隙范围内。如果业务上必须集中插入某个区间的记录,串行化执行反而比并发执行更高效,因为并发只会带来更多的锁等待和上下文切换开销。
5. 第三轮验证:意向锁与表级锁的冲突协作
5.1 实验五:行锁与表级锁的互斥表现
前面已经验证了行级锁和间隙锁的行为,现在来验证一下意向锁和表级锁的关系。先直接看实验:
事务A执行:
sql复制BEGIN;
SELECT * FROM `user_info` WHERE `id` = 3 FOR UPDATE;
事务A现在持有 id=3 这一行的排他锁,同时自动获取了表级意向排他锁(IX锁)。然后事务B执行表级写锁:
sql复制LOCK TABLES `user_info` WRITE;
这条语句会阻塞,原因在于 LOCK TABLES ... WRITE 需要拿到表的排他锁(X锁),而事务A已经持有表级的意向排他锁(IX锁),X锁和IX锁之间冲突。反过来,如果事务A执行 LOCK TABLES user_info WRITE,再让事务B尝试更新某个行,同样会阻塞,因为表级X锁会把所有行级操作都挡在外面。
那如果把事务B换成表级读锁呢?执行:
sql复制LOCK TABLES `user_info` READ;
结论也一样会被阻塞,因为事务A的IX锁与S锁也不兼容。所以只要还有一个事务在某个行上有排他锁,包括读锁在内的任何表级锁都拿不到。这个机制保证了行级锁和表级锁之间的冲突检测不会因为粒度不同而漏掉。
5.2 意向锁的底层设计逻辑
既然谈到这里,顺便把意向锁的设计逻辑讲透。很多初学者第一次看到意向锁时都会有个疑问:“为什么InnoDB要在行锁之上再额外加一个表级意向锁?这不是多此一举吗?”
答案在于加锁效率。假设没有意向锁,事务B想要给整张表加表级锁时,为了判断表里有没有行锁,就必须逐行检查每一条记录的锁状态,一旦表里有几百万行,这个检查代价是灾难性的。有了意向锁之后,每个事务在加行锁前先在表级别标记一下“这里有X型或S型的意向锁”,后续事务想加表级锁,只需要检查意向锁的兼容性即可,不用扫描任何数据行。
用生活化的方式类比:意向锁类似于会议室门口的名牌。某个团队进入会议室前先在门口挂上自己的牌子,其他人想整层租用这间会议室时,看一眼门口有没有牌子就能判断里面有没有人,而不是推开门逐张检查椅子上有没有坐人。这个比喻虽然简单,但非常贴合意向锁存在的意义。
从兼容矩阵来看:
| 锁类型 | IS | IX | S | X |
|---|---|---|---|---|
| IS | 兼容 | 兼容 | 兼容 | 不兼容 |
| IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
| S | 兼容 | 不兼容 | 兼容 | 不兼容 |
| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
IS和IX自身都互相兼容,因为它们代表的是“未来要在某些行上加锁”的意图,而不是已经加锁的事实。S和X之间的互斥关系则是经典的读写互斥原则。这张表是所有锁冲突判断的基础,建议收藏。
6. 死锁复现与锁等待排查技巧
6.1 经典死锁场景复现
死锁是锁机制中最让人头疼的问题。我先用最经典的交叉更新场景把它复现出来。
事务A执行:
sql复制BEGIN;
UPDATE `user_info` SET `age` = 30 WHERE `id` = 1;
事务B执行:
sql复制BEGIN;
UPDATE `user_info` SET `age` = 40 WHERE `id` = 5;
此时两边各持有一行锁,互不干扰。接下来,事务A继续更新 id=5:
sql复制UPDATE `user_info` SET `age` = 30 WHERE `id` = 5;
事务A会阻塞,因为 id=5 的行锁还掌握在事务B手里。然后事务B更新 id=1:
sql复制UPDATE `user_info` SET `age` = 40 WHERE `id` = 1;
事务B同样会阻塞,因为 id=1 的行锁被事务A掌握。死锁形成了。MySQL的死锁检测机制(默认开启)会很快介入,回滚其中一个事务来打破循环。在我执行完第四条SQL后大约不到一秒,MySQL就返回了错误:
text复制ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
注意一个细节:MySQL回滚的是开销更小的事务,不一定是后发起请求的那个。回滚后另一个事务继续执行成功,这是InnoDB的一个自动化决策,代价是业务层需要捕获这个错误并做重试。
6.2 死锁日志解读方法
死锁出现后,第一时间要去看死锁日志。通过 SHOW ENGINE INNODB STATUS\G 可以查看最近一次死锁的完整信息,重点关注 LATEST DETECTED DEADLOCK 这一段。日志里会列出死锁涉及的两个事务,每个事务持有锁的 space id、page no、n bits 等物理信息,以及它们分别在等待哪一行锁。
日志中两种事务类型之间的转换经常见:WAITING FOR THIS LOCK TO BE GRANTED 表示事务在等待锁,HOLDS THE LOCK 表示事务持有了锁。把这两个事务的操作路径画出来,就能一目了然地看到环形依赖。实际操作中,我一般会对照业务代码去分析哪条SQL先执行、两个SQL的加锁顺序是否一致。
例如刚才复现的死锁,日志里就能看到事务A持有 id=1 的记录锁、等待 id=5 的记录锁,事务B持有 id=5 的记录锁、等待 id=1 的记录锁。分析完原因,修复方案通常是让所有事务按相同顺序更新数据,或者尽量缩小事务的更新范围,减少锁重叠的概率。
6.3 排查锁问题的SQL
死锁只是锁问题的一种,更常见的是锁等待超时。要排查锁等待,MySQL提供了几个非常有用的系统表,在8.0版本中主要查 performance_schema:
查询当前所有运行中的事务:
sql复制SELECT * FROM performance_schema.data_locks;
这张表会列出当前所有锁记录,包括锁的类型(LOCK_MODE)、锁的状态(LOCK_STATUS)、锁定的是表还是记录(LOCK_TYPE),以及对应的 OBJECT_SCHEMA 和 OBJECT_NAME。配合 data_lock_waits 表,可以查到每个阻塞关系的发起方和等待方:
sql复制SELECT
requester.THREAD_ID AS waiting_thread_id,
requester.OBJECT_NAME AS table_name,
blocker.THREAD_ID AS blocking_thread_id,
blocker.OBJECT_NAME AS blocked_table
FROM performance_schema.data_lock_waits;
再结合 sys.innodb_lock_waits 视图,可以直接看到阻塞与被阻塞的SQL语句、运行时长和连接信息。我用这套查询在事故现场经常能快速定位到“哪条SQL在等待、是谁一直持有锁不释放”。在5.7版本里对应的表是 information_schema.innodb_trx、innodb_lock_waits、innodb_locks,字段名略有差异,但查询逻辑是一样的。
另外有一个特别实用的排查技巧:优先看事务的运行时间。如果某个事务已经跑了很久还没提交,它持有的锁就会一直不释放,拖住后面所有相关操作。我一般会先把“活跃时间最长但一直没结束的事务”找出来,看它开启时执行了哪些SQL,往往能直接找到问题代码。
7. 锁机制在实际业务中的避坑指南
7.1 索引与锁范围的关系:一个常见误区的验证
锁的范围和索引有着密不可分的关系。我特意用 age 字段做一个等值查询实验来验证:事务A执行 SELECT * FROM user_info WHERE age = 24 FOR UPDATE。age=24 只有一条记录,且 age 列存在辅助索引 idx_age,所以这次查询能通过辅助索引精确定位到 id=3 的记录。加锁范围是辅助索引上的记录锁 (age=24, id=3) 加上主键索引上的记录锁 id=3,锁的范围依然是精确的单行。
那如果我把条件换成一个没索引的列呢?比如 name 列没有任何索引,执行 SELECT * FROM user_info WHERE name = '李四' FOR UPDATE。由于InnoDB无法通过索引定位记录,就必须扫描全表来判断哪些行满足条件,最终结果是在所有扫描过的记录上加锁。表面上看只更新了一条记录,实际上整张表的所有行都被锁住了,任何其他事务的更新、插入、删除都会卡住。这就是典型的“行锁升级为表锁”场景,但准确地说,是因为无法走索引而导致的所有行都被加锁,并非锁机制自动升级。
所以业务上有一个铁律:更新和删除操作务必确保条件字段有索引。尤其要警惕那些“根据业务编号从另一个系统查出来,再用非索引字段更新”的场景,条件字段一旦没有索引,高并发下几乎必然造成锁风暴。
7.2 事务长度与锁持有时间:为什么短事务是万金油
锁是事务执行期间一直持有的,事务提交或回滚后才会释放。这意味着锁的持有时间直接取决于事务的执行时间。一个事务从 BEGIN 到 COMMIT 之间即使只更新了一行数据,只要中间掺杂了外部接口调用、远程RPC、批量循环等慢操作,这行锁就会在整个等待期间一直被持有。
我在排查线上问题时多次见过这种情况:某个服务在事务里调用了另一个服务的HTTP接口,接口响应超时3秒,事务里的锁就多持有3秒;如果接口超时又触发了重试机制,锁的持有时间会被无限拉长,最终把数据库的并发线程数全部拖满。
规避办法有几种:第一,事务中只执行纯数据库操作,把外部调用移出事务边界;第二,尽量用批量SQL代替循环逐条更新,减少往返次数;第三,给事务设置合理的超时时间,防止单条SQL或事务执行过久;第四,在代码层面捕获死锁和锁等待异常,做有限次数的重试,而不是直接把异常抛给用户。
7.3 隔离级别、MVCC与锁的协作:不同场景的取舍
最后说说隔离级别对锁行为的影响。默认的 RR(REPEATABLE READ) 隔离级别下,InnoDB会用临键锁来避免幻读,但代价是锁范围更大、并发度更低。如果把隔离级别调到 RC(READ COMMITTED),间隙锁会被禁用,只保留记录锁,锁范围显著缩小,这也是很多互联网公司为了高并发选择RC的原因。
但RC有一个问题:不可重复读和幻读可能重新出现。RC下执行两次相同查询可能会得到不同的结果,对于金融、订单等强一致性场景来说不可接受。如果业务可以接受快照读,MVCC本身已经提供了无锁读取的能力,那么RC并不影响普通读的体验,只是在写写冲突和部分特殊场景下需要业务侧做补偿。
我给出的实际建议是:核心交易链路如果不希望出现幻读,保持RR;分析类、报表类、高并发列表页等对一致性要求没那么严的场景,切到RC往往能带来肉眼可见的性能提升。切换之前,最好在测试环境模拟真实并发流量,观察锁等待次数和死锁日志是否有明显变化。
就我个人而言,这次的实验让我养成了几个习惯:遇到锁等待先查 performance_schema 而不是重启数据库;写更新SQL之前先看执行计划确认是否走索引;所有事务都尽量保持短小。这套方法论我后面在多次生产事故里反复验证过,效果非常稳定。如果你也想彻底吃透InnoDB的锁机制,强烈建议照着上面的实验自己跑一遍,很多事情只有亲眼看到阻塞发生时,才能真正理解锁在数据库里扮演的角色。
