一条UPDATE,锁了整张表
发布时间:2026-06-26 03:14 浏览量:2
上周三凌晨两点,运维群里炸了。
订单服务 QPS 从 2000 掉到个位数,所有订单接口全部超时。数据库连接池打满,CPU 却没跑满。DBA 查了一圈丢回来一句话:
"有人在跑一条 UPDATE orders SET status=2 WHERE status=1,慢查询日志躺了二十分钟。"
一条 SQL,锁住了三千万行的订单表。不是死锁,不是慢查询,是锁的范围超出了所有人的预期。
这篇文章就拆这个坑。核心结论说在前面:InnoDB 的行锁不是按行加,是按索引区间加。你以为锁了一行,实际上它锁了一片。
搭一张订单表:
-- status 上建了普通索引,非唯一CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, status INT NOT NULL DEFAULT 0, amount DECIMAL(10,2), create_time DATETIME, INDEX idx_status (status)) ENGINE=InnoDB;INSERT INTO orders VALUES(1, 1001, 0, 99.00, '2026-06-20 10:00:00'),(5, 1002, 0, 199.00, '2026-06-20 10:01:00'),(10, 1003, 0, 299.00, '2026-06-20 10:02:00'),(15, 1004, 1, 399.00, '2026-06-20 10:03:00'),(20, 1005, 2, 499.00, '2026-06-20 10:04:00');
打开两个会话:
-- 会话A:想锁第一条未支付订单START TRANSACTION;SELECT * FROM orders WHERE status = 0 LIMIT 1 FOR UPDATE;-- 不提交,锁先挂着-- 会话B:另一个用户来下单START TRANSACTION;INSERT INTO orders VALUES (3, 2001, 0, 149.00, NOW);-- 直接卡住
会话 B 插的 id=3,跟会话 A 锁的 id=1 完全不是同一行。为什么卡住了?
查锁信息:
SELECT object_name, lock_type, lock_mode, lock_dataFROM performance_schema.data_locks;
输出长这样:
idx_status RECORD X,GAP '0',1idx_status RECORD X '0',1idx_status RECORD X,GAP '0',5
X,GAP——间隙锁,把 status=0 的整个索引区间锁死了。任何往这个间隙插 status=0 的操作都排队等。这就是"只见一条 UPDATE,表像被锁死了"的真相。
InnoDB 是索引组织表,锁永远加在索引上,不是加在物理行上。
当 idx_status 是非唯一索引时,WHERE status=0 命中多条记录。在 MySQL 默认的 REPEATABLE READ 级别下,InnoDB 启用 Next-Key Lock——记录锁和间隙锁的组合。
idx_status 的有序结构:
索引区间:(-∞,0] (0,0] (0,0] (0,1] (1,2]对应主键: gap 1 5 10 15
SELECT ... WHERE status=0 FOR UPDATE 锁的是整个 status=0 的索引区间,不是返回的那一行。LIMIT 1 管结果集,不管锁范围。
用 Java 验证:
ExecutorService executor = Executors.newFixedThreadPool(2);// 线程1:对 status=0 加 Next-Key Lock,30秒不提交Future t1 = executor.submit( -> { try (Connection conn = dataSource.getConnection) { conn.setAutoCommit(false); conn.createStatement.execute( "SELECT id FROM orders WHERE status = 0 LIMIT 1 FOR UPDATE" ); System.out.println("线程1:已持有 status=0 区间的 Next-Key Lock"); Thread.sleep(30000); // 模拟长事务 conn.commit; }});// 等线程1拿锁Thread.sleep(500);// 线程2:尝试插入 status=0 的新订单Future t2 = executor.submit( -> { long start = System.currentTimeMillis; try (Connection conn = dataSource.getConnection) { conn.createStatement.execute( "INSERT INTO orders VALUES (3, 2001, 0, 149.00, NOW)" ); System.out.println("插入成功,等待了 " + (System.currentTimeMillis - start) + "ms"); } catch (Exception e) { System.out.println("插入超时,等待了 " + (System.currentTimeMillis - start) + "ms"); }});executor.shutdown;
线程 2 的输出大概率是"等待了 30000ms"。间隙锁和插入意向锁互斥,只能等线程 1 提交。
三种隔离级别下锁行为的差异:
隔离级别
间隙锁
幻读防护
并发度
READ UNCOMMITTED无无极高READ COMMITTED (RC)无无高REPEATABLE READ (RR)有有中
RC 级别下间隙锁直接关闭,上面场景不会发生。这也是互联网公司大多用 RC 的原因——业务层做好幂等和乐观锁,RC 的并发性能优势太明显。
RR 为什么搞出间隙锁?为了防幻读。同一事务内两次范围查询,第二次不能出现第一次没有的新行。InnoDB 用 Next-Key Lock 把这个区间物理锁死。
不是真的 LOCK TABLES,而是"看起来像整张表被锁了"。两个触发条件:
WHERE 条件没走索引 → 全表扫描 → 行行加锁非唯一索引 + RR + 当前读 → Next-Key Lock 锁住大范围区间-- 典型锁表写法:非唯一索引 + RR + FOR UPDATESELECT * FROM orders WHERE status = 0 FOR UPDATE;-- 更致命的:expire_time 没索引,全表扫描,行行加 X 锁SELECT * FROM orders WHERE expire_time
线上故障,先止血再根治。三步走。
第一步:定位元凶
SELECT r.trx_id AS waiting_trx, r.trx_mysql_thread_id AS waiting_thread, b.trx_id AS blocking_trx, b.trx_mysql_thread_id AS blocking_thread, TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW) AS wait_secondsFROM information_schema.innodb_lock_waits wJOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_idJOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
找到阻塞者,KILL 掉立即止血。
第二步:改 SQL,两段式加锁
-- ❌ 非唯一索引上直接 FOR UPDATE,锁整个区间SELECT * FROM orders WHERE status = 0 LIMIT 1 FOR UPDATE;-- ✅ 先快照读拿主键,再按主键加行锁SELECT id FROM orders WHERE status = 0 LIMIT 1; -- 快照读,无锁SELECT * FROM orders WHERE id = ? FOR UPDATE; -- 主键上只锁一行
主键是唯一索引,等值查询退化为 Record Lock,不触发间隙锁。
第三步:根治
方案
适用场景
代价
改为唯一索引语义上真的唯一(如订单号)改表结构降级 RC 隔离级别高并发业务放弃幻读防护SKIP LOCKED任务队列(抢单、发券)仅 MySQL 8.0+两段式查询通用方案多一次查询
任务队列场景,SKIP LOCKED 性价比最高:
SELECT id FROM ordersWHERE status = 0ORDER BY create_timeLIMIT 1FOR UPDATE SKIP LOCKED;
不等待,不扩大锁范围,不堆积连接。
InnoDB 的锁不是按你看到的那几行加。非唯一索引 + RR + 当前读,锁的是索引区间。LIMIT 管结果集,不管锁范围。你改不了 InnoDB 的锁规则,但可以改索引设计、改隔离级别、改查询方式——这三样够你避开 90% 的锁表事故。