select ... for update 会锁整张表而非仅行锁,是因为未使用索引、索引失效(如函数操作、隐式转换、不满足最左前缀)导致全表扫描,innodb 退化为表级锁。

为什么 SELECT ... FOR UPDATE 会锁整张表而不是只锁行
MySQL 的行锁生效前提是用了索引且查询条件能命中索引。如果 WHERE 字段没索引、用了函数(如 YEAR(created_at))、或者走了全表扫描,InnoDB 就会退化为表级锁——尤其在 READ COMMITTED 以下隔离级别更常见。
- 检查执行计划:
EXPLAIN SELECT ... FOR UPDATE,确认type是ref/range而非ALL - 避免在索引字段上做运算或隐式类型转换,比如
user_id = '123'(user_id是INT)会失效索引 - 联合索引要注意最左前缀,
(a,b,c)上查WHERE b = ?不走索引,照样可能锁表
大表 UPDATE 或 DELETE 时怎么避免长时间锁表
一次性更新百万行?那不是在改数据,是在给其他事务发“请勿打扰”通知。InnoDB 的行锁是逐行加的,事务越长,锁持有时间越久,竞争越猛。
- 拆成小批量操作:用
WHERE id BETWEEN ? AND ?或WHERE id > ? ORDER BY id LIMIT 1000分页处理 - 每次提交后休眠几十毫秒(
SLEEP(0.05)),缓解主从延迟和锁堆积 - 避开业务高峰执行;DDL 操作(如加索引)也别直接
ALTER TABLE,用pt-online-schema-change或gh-ost
事务里混用 SELECT 和 UPDATE 为什么更容易锁冲突
事务不是“开始就锁”,而是“用到才锁、提交才放”。但如果你先 SELECT ... FOR UPDATE 查一批 ID,再循环 UPDATE 这些 ID,中间任何一步出错或延迟,都会让第一批锁一直挂着。
- 尽量把逻辑压进单条语句,比如用
UPDATE ... WHERE id IN (SELECT id FROM ...)(注意子查询不能引用同一张表) - 如果必须分步,确保
SELECT和后续UPDATE用完全相同的索引和条件,避免幻读触发间隙锁扩展 - 设短超时:
SET innodb_lock_wait_timeout = 10,别让一个卡住的事务拖垮整个连接池
什么时候该考虑水平拆分而不是继续优化单表
当单表超过 2000 万行、写入 QPS 稳定超过 500、且 SHOW ENGINE INNODB STATUS 里频繁看到 lock struct(s) 数量飙升,说明锁资源已成瓶颈,优化边际收益急剧下降。
- 优先按业务维度拆,比如订单按
user_id % 4分 4 张表,比按时间范围更利于关联查询和缓存 - 拆分后保留原
id作为逻辑主键,用自增shard_id做物理主键,避免分布式 ID 生成器成为新瓶颈 - 跨分片
JOIN和全局ORDER BY几乎必然慢,得靠应用层聚合或异步宽表补位
锁竞争的本质是资源争抢,不是语法问题。调优到最后,往往不是“怎么写更高效”,而是“谁该在什么时候动哪部分数据”——这个判断比写十条 EXPLAIN 更难,也更容易被忽略。











