1. 从内存锁到数据库锁:为什么我们需要重新审视锁
先聊一个近期让我印象很深的问题背景。某个周五晚上,业务群里突然有人说“订单列表打不开了”,紧接着就是“后台保存也报错了”。我登陆数据库一看,SHOW PROCESSLIST 里挂了一排 Waiting for table metadata lock。当时的第一反应不是去看慢查询,而是去查 information_schema.innodb_trx,果然发现一条几乎跑了半小时的事务还死死攥着某张表的元数据锁。那一刻我意识到,很多人(包括我)在应用层写惯了 synchronized 和 ReentrantLock,一进入 MySQL 的世界,反而对锁的玩法和坑特别陌生。
这也正好是我这篇“锁机制(下)”想写透的事。上一篇我们聊了并发编程中的锁:偏向锁、轻量级锁、自旋锁、AQS,还有分布式锁那套跨进程互斥的思路。但那次聊完以后,我收到不少反馈,说真正让他们头疼的其实是数据库层面的锁,尤其是 MySQL 的锁表机制。一条简单的 UPDATE 没走索引,居然能把整张表冻住;一个长时间不提交的事务,居然能让所有写操作排队。这些现象背后的原理,和应用层的锁模型有联系,但并不同构。
先说一个我的核心结论:数据库锁不是“锁数据行”那么简单,它锁的是索引记录、区间、元数据甚至整张表,锁的粒度取决于存储引擎、隔离级别、索引条件和执行计划。而 MySQL 里最常见的故障形态就是“锁表”,表现为某个会话持有排他锁,后续所有对该表的写入都被堵住,连接数迅速累积,最后整个服务不可用。
很多开发者学习锁机制时,容易踩三个误区。
第一个误区是把“MySQL 锁”和“InnoDB 行锁”划等号。实际上 MyISAM 引擎只有表锁,InnoDB 才有真正的行锁;而且 InnoDB 的行锁必须建立在索引之上,如果查询无法使用索引,行锁就会退化成大范围的锁,看起来和表锁没什么区别。
第二个误区是认为“事务提交后锁一定会立刻释放”。这句话大方向没问题,但间隙锁和临键锁在 REPEATABLE READ 隔离级别下,可能在事务提交后才彻底释放。如果事务体很大,持锁时间就会被拉长,等待队列里的事务就会堆积。
第三个误区是认为死锁是小概率事件。在高并发下,只要多个事务对多张表或者多个索引的加锁顺序不一致,死锁几乎必然出现。问题不在于“会不会死锁”,而在于你有没有死亡检测机制,以及日志记录能不能支撑你快速定位。
所以,对锁机制的理解,我建议至少分成三层:进程内锁、数据库存储引擎锁、跨进程分布式协调锁。这篇聚焦中间这一层:以 InnoDB 为主线,把表锁、行锁、间隙锁、元数据锁,还有锁等待和死锁的排查方法,全部串起来讲一遍。
理解这些内容之后,你至少能拿到三样东西:第一,以后再遇到“数据库卡住”这类故障,知道该去哪个视图查锁;第二,能根据 EXPLAIN 的结果判断一条 SQL 是否会造成锁表;第三,在设计高并发事务时,清楚为什么要控制事务大小、为什么要保证索引有效、为什么加锁顺序最好一致。
这部分的重点不是背参数,而是建立一个判断框架:任何锁的引入,都是并发控制与性能之间的取舍。你看到锁等待,第一反应不该是“杀掉进程”,而是“为什么这个事务需要持有这么久的锁”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MySQL 锁表机制:一条 UPDATE 让全库卡顿的真相
我们经常在故障复盘时听到“锁表”这个词,但“锁表”在 MySQL 里其实有好几种完全不同的实现源头。我把它们拆开讲,不然排查时会走很多弯路。
2.1 Server 层表锁与存储引擎层锁是两套体系
MySQL 的架构是分层的:Server 层负责连接管理、SQL 解析、优化和执行;存储引擎层负责数据读写和事务控制。表锁这个操作,在 Server 层和 InnoDB 层都存在,但它们不是同一个东西。
Server 层的表锁,最常见的是手动执行 LOCK TABLES table_name WRITE 这种命令。一旦执行,其他会话对该表的读写都会被阻塞,直到执行 UNLOCK TABLES 或会话断开。这种锁在日常业务代码里一般不会主动使用,但在一些维护脚本中偶尔能看到。如果你在 PROCESSLIST 里看到 Waiting for table level lock,多半就和这类锁相关。
InnoDB 引擎层的真正语义是“索引锁”和“范围锁”,它理论上不排斥并发读写。但在实际执行中,如果一条 SQL 无法走索引,InnoDB 会在扫描每一行时都加上锁,最终锁定的记录覆盖了大量数据页,行为上等同于锁表。更麻烦的是,这种“隐式锁表”不像显式 LOCK TABLES 那么直观,从 SHOW PROCESSLIST 里看到的只是普通的 Updating 或 Locked,如果不结合 EXPLAIN 和锁等待视图,很容易误判为慢 SQL。
2.2 锁模式:读写锁的基本盘
MySQL 里的锁模式虽然数量不少,但核心规律可以简化成一句话:读读共享、读写互斥、写写互斥。InnoDB 层面的锁类型包括:共享锁(S)、排他锁(X)、意向共享锁(IS)、意向排他锁(IX)。其中意向锁是表级锁,用来快速判断表中是否已有行级锁,避免逐行检查。
| 锁模式 | 简称 | 兼容性 | 典型场景 |
|---|---|---|---|
| 共享锁 | S | 与S兼容,与X互斥 | SELECT ... LOCK IN SHARE MODE |
| 排他锁 | X | 与S/X都互斥 | SELECT ... FOR UPDATE、UPDATE、DELETE |
| 意向共享锁 | IS | 与IS兼容,与IX不冲突 | InnoDB 加行级S锁前自动加 |
| 意向排他锁 | IX | 与IS兼容,与IX不冲突,但与S/X表锁冲突 | InnoDB 加行级X锁前自动加 |
上面的表格里有个反直觉点:IS 和 IX 之间居然不冲突。这是为什么呢?因为意向锁只是向表级别“打个招呼”,告诉别的事务“我稍后可能会在某一行上加锁”,真正的互斥判断要到行级别去做。如果两个事务都只是意向锁,它们将来操作的行不同,完全没必要在表级别就互相等待。
2.3 表锁拖垮全库的真实案例
我讲一个线上案例,这可能是我遇到过最典型的“锁表”事故。
某天中午,一个定时任务对订单表执行批量更新:
sql复制UPDATE orders SET status = 'EXPIRED' WHERE expire_time < NOW();
这张表有 3000 万行,expire_time 字段没有索引。优化器只能选择全表扫描。InnoDB 在扫描过程中,对每一行读取并尝试加锁,最终相当于给整张订单表的所有记录都加了排他锁。此时:
- 前台用户提交订单,
INSERT被阻塞。 - 后台修改订单备注,
UPDATE被阻塞。 - 连某些
SELECT ... FOR UPDATE的查询也阻塞了。 - 普通
SELECT靠 MVCC 快照读还能执行,但连接池中的连接逐渐被等待事务占满。
最后表现为:数据库 CPU 不高,IO 不高,但所有业务都在等锁。很多人看到“数据库卡死”就以为是慢查询拖垮了性能,其实真正的元凶是持锁范围过大。
排查过程里,information_schema.innodb_trx 里能看到这条 UPDATE 事务已经执行了很久,trx_rows_locked 数值巨大,基本可以断定是全表范围的锁。解决方式并不复杂:先给 expire_time 建索引,然后分批更新,每批 1000 条,中间提交一次。改完之后,这把锁从“全表”缩小成“一批记录”。
教训很简单:写 UPDATE/DELETE 之前,一定要看执行计划。不是“我更新了一行”就代表只锁一行,没有索引辅助定位,InnoDB 无法精确锁定,只能锁一个范围。这个现象在 MySQL 官方文档里叫作“锁扩散”。
2.4 AUTO-INC 锁:自增主键也有可能在锁表
还有个经常被忽略的表级锁是自增锁。InnoDB 在自增主键分配上有一个内部机制,专门避免两个事务拿到相同的自增值。innodb_autoinc_lock_mode 有三个取值:
- 0:传统模式,插入时总是持有表级 AUTO-INC 锁,直到 SQL 结束。
- 1:连续模式(默认),普通插入使用轻量互斥量,只有批量插入才使用表级锁。
- 2:交错模式,所有插入都不使用表级锁,但并发插入时自增值可能不连续。
默认值 1 在大部分场景下都没问题。但如果你执行的是 INSERT INTO ... SELECT 这样的批量插入,InnoDB 可能会在分配自增 ID 期间持有表级锁,导致其他会话的插入操作排队。批量导数的夜间任务尤其容易触发。
遇到“并发插入全部卡住”的时候,如果行锁的排查没有结论,多看一眼自增锁参数和会话状态。特别是大量批量插入的场景,AUTO-INC 锁是一个非常容易被忽视的源头。
3. InnoDB 行锁:你以为锁的是行,其实锁的是一个区间
如果说表锁的问题还比较好理解,那行锁就复杂很多。InnoDB 的行锁,尤其在 REPEATABLE READ 隔离级别下,并不总是单行锁。它可能在索引记录之间留下“间隙锁”和“临键锁”,目的是防止幻读。理解这一点,很多“不知道为什么插入卡住”的问题就能解释通了。
3.1 记录锁、间隙锁、临键锁、插入意向锁的完整关系
这四个概念经常出现,但很少放在一起对比。我总结了一下它们的角色和差异:
| 锁类型 | 锁定的对象 | 解决的问题 | 常见触发场景 |
|---|---|---|---|
| 记录锁(Record Lock) | 单一索引记录 | 只保护一条记录 | 唯一索引等值查询 |
| 间隙锁(Gap Lock) | 索引记录之间的空隙 | 防止其他事务插入记录 | 范围查询、非唯一索引等值查询 |
| 临键锁(Next-Key Lock) | 记录 + 前一个间隙(左开右闭) | 防止幻读,保护区间 | 普通索引范围扫描 |
| 插入意向锁(Insert Intention Lock) | 插入前的间隙声明 | 协调多个插入对同一间隙的等待 | INSERT 前自动加 |
我比较喜欢把临键锁理解为“带护栏的占位”。它锁住的不只是目标记录,还包括记录前面的空隙。这样,其他事务既不能修改这条记录,也不能在空隙中插入新记录,从而在索引层避免幻读。但代价是并发插入的灵活性变低了——两个事务如果试图插到同一个间隙里,就会出现锁等待,甚至死锁。
3.2 为什么没有索引会导致“行锁升级为表锁”
先说一个最容易被误解的点:InnoDB 没有真正的“升级锁”机制,它不会把行锁从概念上升级成表锁,但是它的行为会让锁的范围扩散到整个扫描区间。
举个例子:
sql复制DELETE FROM coupon WHERE status = 'USED';
status 字段没有索引。MySQL 要做的事是全表扫描,找到所有 status='USED' 的记录,然后逐条加排他锁并删除。扫描过程中,初始有几条记录被锁住,但随着扫描范围扩大,锁覆盖的行越来越多。在另一个会话看来,几乎所有需要写入的请求都在等待,和表锁的体验完全一样。
这种情况的排查点很明确:看 EXPLAIN DELETE FROM coupon WHERE status = 'USED' 的输出,如果 type 是 ALL 或者 key 是 NULL,那这条 SQL 的锁范围大概率已经失控。解决方式不是依赖数据库的锁升级机制,而是从索引这一层下手。
3.3 间隙锁导致的并发插入卡死
还有一个高频现场:事务 A 执行了某个范围查询,事务 B 插入一条满足该范围的新记录,结果插入一直等待。事务 A 其实并没有修改任何一条存在的记录,但它持有间隙锁,B 的插入需要进入同一个间隙,于是被阻塞。
这在隔离级别为 REPEATABLE READ 时尤其明显。举个例子:
sql复制-- 事务 A
START TRANSACTION;
SELECT * FROM product WHERE price BETWEEN 100 AND 200 FOR UPDATE;
-- 事务 B
INSERT INTO product (id, name, price) VALUES (9999, '新商品', 150);
事务 B 想插入一条 price=150 的新商品,但这条记录落在事务 A 锁定的间隙范围内,所以 INSERT 会一直等待,直到事务 A 提交或回滚。
间隙锁不是 bug,它是 MySQL 为了保证 REPEATABLE READ 下不出现幻读而设计的。但如果你确认业务可以通过唯一索引等方式消除幻读风险,并且对并发插入的要求更高,可以评估把隔离级别改成 READ COMMITTED。在这个隔离级别下,InnoDB 会禁用间隙锁,只保留记录锁。这样并发插入的冲突会明显减少,但业务层需要额外处理重复读问题。
我个人对这个改动的态度是谨慎的。因为事务隔离级别是全局参数,下调后影响的绝不止一条 SQL,而是整个实例下所有事务。如果某个历史业务逻辑不小心依赖了 REPEATABLE READ 下的可重复读语义,改完可能引发“同一事务两次查询结果不一致”的诡异问题。更稳妥的做法是:先检查索引设计,看能否通过唯一索引把范围查询变成等值查询,从而减少间隙锁范围。
3.4 用一个小实验看清行锁行为
纸上谈兵不如实际感受一下。开两个 MySQL 会话,按下面步骤操作:
会话 A:
sql复制START TRANSACTION;
SELECT * FROM orders WHERE order_no = '202401010001' FOR UPDATE;
此时查询命中唯一索引 order_no,加的是记录锁。
会话 B:
sql复制UPDATE orders SET status = 1 WHERE order_no = '202401010001';
会话 B 会进入锁等待状态。在会话 B 的会话里执行 SHOW PROCESSLIST,能看到状态是 Updating。如果等待超过 innodb_lock_wait_timeout 默认的 50 秒,就会报一个 Lock wait timeout exceeded。
如果改了条件,使用非唯一索引:
sql复制-- 会话 A
SELECT * FROM orders WHERE status = 'UNPAID' FOR UPDATE;
由于 status 是非唯一索引,InnoDB 会对所有 status='UNPAID' 的记录以及它们之间的间隙加临键锁。此时其他会话即使想插入一条 status='PAID' 的记录,也可能因为间隙重叠而等待。这就是为什么“我只查了一批数据,为什么插入被卡住了”的原因。
掌握行锁的关键在于:锁的锁主不是数据行,而是索引记录。查询条件能走到什么索引,决定了锁的范围是单个点还是一整个区间。
4. 锁等待排查实录:是谁锁住了我的表
排障环节永远是干货需求最大的部分。这一节我分享一套在线上环境验证过很多次的排查路径,从“看到卡死”到“抓到元凶”,十分钟内基本能完成。
4.1 第一件事:看会话状态和事务列表
当数据库出现锁等待症状时,第一个动作是执行:
sql复制SHOW FULL PROCESSLIST;
重点看 State 列:
Waiting for table metadata lock:元数据锁等待,通常和 DDL 或长事务有关。Waiting for table level lock:表级锁等待,可能是显式 LOCK TABLES。Updating/Locked:行级锁等待,可能正在等待某一行或某个区间的锁。
看到这些状态后,下一步立即查事务表:
sql复制SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_locked
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;
trx_started 最早的事务,往往是问题的源头。trx_rows_locked 如果特别大,说明该事务锁定了大量行,需要重点怀疑。
4.2 第二步:反查阻塞关系链
MySQL 5.7 以上版本,information_schema 里有锁等待关系视图,可以用来定位“谁在等谁”。我常用的查询长这样:
sql复制SELECT
r.trx_id AS waiting_trx,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
这条 SQL 会把“等待事务”和“阻塞事务”配对输出。拿到 blocking_thread 后,回到 PROCESSLIST 里找对应的线程 ID,基本就能看到阻塞源的 SQL 是什么。
到了这里,多数问题已经水落石出。剩下的问题是策略选择。
MySQL 8.0 之后,锁信息更丰富,可以用:
sql复制SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;
data_locks 能看到每个锁对应的索引名、锁类型、锁模式,比 5.7 的 innodb_locks 更直观。
4.3 第三步:怎么处理锁等待
锁源抓出来后,处理顺序非常重要。我个人的优先级是:
- 如果阻塞事务已经运行很久,而且明显不是业务正常行为(比如已经空闲了十几分钟),直接
KILL blocking_thread。这是止血最快的办法。 - 如果阻塞是一条业务 SQL 正在正常执行,只是持锁时间过长,不要急着 KILL,先优化它的执行计划。比如加索引、改查询条件、拆小事务。
- 如果等待者已经堆积很多,可以临时调小
innodb_lock_wait_timeout,让业务快速失败而不是无限排队,缓解连接池被占满的风险。但这永远是临时方案,不是根治手段。 - 对批量任务,务必改成切片执行。每个切片提交后释放锁,给其他事务让出机会。
4.4 一个容易被忽略的坑:事务里夹了远程调用
有一次排查时,我锁定了阻塞源:一个更新配置表的简单事务。SQL 本身只更新一条记录,按理说不会持锁太久,但 trx_started 显示它已经运行了 30 分钟,而且状态一直是 Sleep。
后来查应用日志才发现,代码里的事务边界是这样的:
java复制@Transactional
public void updateConfig() {
// 更新配置表
updateConfigTable();
// 调用远程接口,耗时可能几十秒
remoteService.sync();
}
事务把远程调用包了进去。数据库锁不释放,是因为事务一直开着,而持锁时间取决于远程接口的耗时。这就完全解释了为什么一条简单的更新会导致一堆写入等待。
这种问题在代码评审时容易发现,但很多团队的评审往往只关注业务逻辑,不关注事务边界和锁范围。我建议大家养成的习惯是:事务里不做 IO 操作,不做网络调用,不 sleep,只做数据库操作。如果必须远程调用,把事务拆开,让数据库的锁快速释放。
4.5 MDL 锁:DDL 引发的“雪崩”
最后单独讲一下元数据锁(MDL)的原因,因为它的故障形态和行锁、表锁都不同。
MySQL 5.7 以后,执行 DDL 语句(如 ALTER TABLE)时,需要获取表的元数据锁。此时如果有一个长事务正在查询这张表,DDL 就会进入 Waiting for table metadata lock 状态。更糟的是,MDL 有一个排队机制:一旦 DDL 排队,后续对该表的所有读写请求也会排队,最终结果就是整张表看起来“不可用”。
这个锅经常甩给慢查询,实际元凶却是那个开了事务但一直不提交的业务连接,或者一条 ALTER TABLE 加字段的操作没有设置超时时间,在等待中把后面的请求全部堵死。
处理办法有两步:第一步,杀掉排在最前面的阻塞线程,让 DDL 拿到锁;第二步,治理 DDL 的执行方式,用工具(如 pt-online-schema-change)执行在线表结构变更,并设置合理的 lock_wait_timeout,绝不能让 DDL 无限等下去。
排查锁等待,最忌讳的是“只看一个视图就下结论”。正确姿势是把
PROCESSLIST、innodb_trx、innodb_lock_waits三个信息源交叉验证,先定位锁源,再决定操作。
5. 锁参数配置与设计避坑:让数据库少踩锁坑
前四节已经讲完了锁的机制和排障方法,这一节把日常开发和运维中真正有用的参数、设计原则和监控方案收个尾。这些内容是我在多次线上事故之后总结出来的,值得直接抄作业。
5.1 核心参数速查
| 参数 | 默认值 | 作用 | 经验建议 |
|---|---|---|---|
| innodb_lock_wait_timeout | 50 | 行锁等待超时时间(秒) | 没有特殊业务要求,建议降到 10~20 |
| lock_wait_timeout | 31536000 | 元数据锁等待超时时间(秒) | 必须显式设置,比如 28800,避免 DDL 永远挂着 |
| innodb_autoinc_lock_mode | 1 | 自增锁分配模式 | 默认即可;大批量插入时要关注 |
| transaction_isolation | REPEATABLE-READ | 事务隔离级别 | 业务允许时,可评估降到 READ-COMMITTED |
| innodb_deadlock_detect | ON | 死锁检测 | 8.0 安全项,开启状态不要随意关闭 |
这里要特别提醒 lock_wait_timeout,它和 innodb_lock_wait_timeout 是完全不同的两个参数。前者管元数据锁等待,默认值是惊人的一年(31536000 秒);后者管行锁等待。很多 DDL 卡住后无限排队,就是因为 lock_wait_timeout 太大,MySQL 根本不会主动终止 DDL 等待。
5.2 事务设计的四个原则
锁表和高锁等待,多数时候不是数据库配置问题,而是事务设计问题。四条原则我在团队里反复强调:
- 事务要短。不要在一个事务里做太多操作,更不要把远程调用、消息发送、文件上传之类的外部 IO 包进来。
- WHERE 条件尽量走唯一索引/等值查询。这样锁的对象是明确的记录,而不是大范围区间。如果只能走范围查询,对锁的代价要心里有数。
- 多个事务操作多张表时,加锁顺序要一致。例如先更新用户表,再更新订单表,所有代码都遵守这个顺序,死锁概率会大大下降。
- 批量操作用切片提交。每批几百到一千条,处理完立即提交,让锁滚动释放。哪怕任一切片失败,回滚成本也可控。
这些原则不是理论,每一句都对应着一个我见过的事故。事务里放远程调用那个案例就不重复了;加锁顺序不一致导致的死锁,在多个微服务并行修改同一组表时尤其常见,后来我们直接在 SQL 评审清单里加了一个硬性字段:多表操作时,请注明所有表的加锁顺序。
5.3 监控锁的长期方案
锁问题基本不可能靠一次排查就彻底消灭,所以还需要把监控做成日常机制。
MySQL 8.0 优先使用 performance_schema.data_lock_waits 和 data_locks 视图。它们比 5.7 的 innodb_lock_waits 信息更完整,能看到索引信息和锁对象。如果线上还是 5.7,继续用 information_schema。
死锁日志也不可忽视。SHOW ENGINE INNODB STATUS 里的 LATEST DETECTED DEADLOCK 段落会记录最近一次死锁的 SQL、锁等待资源和事务栈。我现在的做法是用定时任务把死锁日志采集到独立表,每周复盘一次,高发 SQL 直接进入审核黑名单。
这么做效果很明显。前半年,我们锁相关的线上故障基本归零。不是锁问题消失了,而是大部分锁问题在进入生产环境之前就被拦截了。
还有一条来自实战的额外建议:在应用层给事务设置超时时间。比如 JDBC 的 setQueryTimeout(10),MyBatis 也可以在 SQL 级别配置 timeout。这样即使数据库侧的 innodb_lock_wait_timeout 还没到点,应用层已经能主动抛错并回滚,不会让请求一直悬挂在线程池里。这个经验帮我避免过很多次连接池耗尽的问题。
5.4 锁的分层:到底该用数据库锁还是分布式锁
既然聊到了设计原则,我想再说一个经常被混淆的问题:分布式锁和数据库锁各自的适用场景。
分布式锁(Redis、ZooKeeper、etcd)解决的是跨进程互斥,能让多个服务节点之间对某个资源的访问串行化。但它无法保证数据库事务的 ACID,因为锁的持有和事务的提交之间可能存在时间差。
举个例子:库存扣减。如果用分布式锁包住“查库存、判断库存、扣库存、更新数据库”四个步骤,能保证同一时刻只有一个服务节点执行这段逻辑,但一旦分布式锁的持有者和数据库事务不一致——比如锁已经释放,但数据库事务还没提交——其他节点依然可能读到旧库存。
正确的方式是:库存这种强一致数据,靠数据库行锁或乐观锁来保证;分布式锁只用来控制跨服务、跨资源的全局互斥。现在很多团队容易把分布式锁当万能药,反而忽略了数据库本身的并发控制能力,这是本末倒置。
理解了锁的分层,再回头看“锁表现象”,其实它并不可怕。锁机制本身不是缺陷,它是在并发场景下保证数据一致性的必要代价。真正的问题,往往是我们在索引设计、事务边界、锁参数和监控机制上的功课没做足。
我现在写代码或者做技术方案评审时,习惯先画一张“锁覆盖范围”草图:这个操作会锁哪些资源,锁多久,可能和哪些并发操作冲突,冲突后有没有快速失败策略。这个习惯帮我在设计阶段就排掉了不少潜在的锁问题,而不是等线上告警的时候再去查锁等待。
如果这篇能给你留下一个可执行的习惯,我希望是这条:任何一条 UPDATE、DELETE 或者 SELECT ... FOR UPDATE 语句上线之前,先用 EXPLAIN 看它的访问类型和索引使用情况。这一步做到位,至少能避免一半以上的锁表事故。
