加锁读是什么
加锁读用来显式控制并发:SELECT ... FOR UPDATE 对扫描到的行加排他锁,FOR SHARE 加共享锁。它们只在事务中才有意义,作用是把「读出来判断,再写回去」这段逻辑保护起来,避免两个并发请求读到同一份数据后互相覆盖。代价是加锁读会阻塞其它写,条件没走索引时影响范围会明显放大。
基本信息
| 用途 | 悲观锁读取,保护读改写逻辑 |
|---|---|
| 语句 | SELECT … FOR UPDATE / FOR SHARE |
| 前提 | 必须在事务内,条件列最好有索引 |
| 8.0 增强 | FOR UPDATE SKIP LOCKED、NOWAIT |
| 等待超时 | innodb_lock_wait_timeout,默认 50 秒 |
典型使用场景
- 库存扣减、余额变更这类读改写操作
- 订单状态流转时的并发保护
- 任务队列取任务,用 SKIP LOCKED 避免互相等待
- 对账、结账时锁定关键行
- 排查锁等待与死锁时确认加锁范围
库存扣减范式
库存扣减范式
START TRANSACTION;
SELECT qty FROM stock WHERE sku='SKU-1' FOR UPDATE; -- 排他锁
UPDATE stock SET qty = qty - 1 WHERE sku='SKU-1' AND qty > 0;
COMMIT;
-- 8.0:跳过已锁行,适合任务队列
SELECT id FROM jobs WHERE status='ready' ORDER BY id LIMIT 1
FOR UPDATE SKIP LOCKED;
-- 不想等待,锁不到就立刻报错
SELECT id FROM jobs WHERE status='ready' LIMIT 1 FOR UPDATE NOWAIT;用法与影响
| 写法 | 作用 | 注意事项 |
|---|---|---|
| FOR UPDATE | 排他锁 | 阻塞其它事务读写同一批行 |
| FOR SHARE | 共享锁 | 多个读可以共存,写要等待 |
| SKIP LOCKED | 跳过已被锁定的行 | 队列场景避免互相等待 |
| NOWAIT | 锁不到立即失败 | 适合对响应时间敏感的逻辑 |
| 作用范围 | 取决于扫描的行数 | 无索引条件会锁住大量行 |
条件不走索引时 FOR UPDATE 可能锁住大量行甚至整表,把其它业务拖住;等待时长由 innodb_lock_wait_timeout 控制,默认 50 秒,排查时要一起看。
加锁读必须在事务里才有意义:autocommit=1 时单条语句执行完锁就释放,「读出来再写回去」的竞态依然存在,等于没加锁。用框架封装时尤其要注意,别让事务被提前提交,或者被拆成了两段。
死锁与加锁顺序直接相关:批量加锁的场景要固定访问顺序,例如按主键升序处理,否则两个事务交叉加锁必然互相等待。真的出现死锁后,用 SHOW ENGINE INNODB STATUS 查看最近一次死锁的详细信息。