锁读是什么
锁读在事务里先对行加锁再读写,用来解决「先查后改」的并发竞争,典型场景是任务队列里取一条待处理任务并标记为处理中。MySQL 与 PostgreSQL 都支持 SELECT … FOR UPDATE(排他锁)与 FOR SHARE(共享锁);MySQL 8.0 起用 FOR SHARE 取代旧的 LOCK IN SHARE MODE。锁读必须在事务里才有意义,autocommit 下单条语句结束就释放锁。WHERE 条件走不到索引时锁范围会放大,可能把并发写入整体堵住。
基本信息
| FOR UPDATE | 排他锁,其它事务的锁读与写会等待 |
|---|---|
| FOR SHARE | 共享锁,MySQL 8.0 起取代 LOCK IN SHARE MODE |
| 前提 | 必须在事务内使用,否则锁随语句结束即释放 |
| 行锁条件 | WHERE 要走索引,否则可能升级为更大范围的锁 |
| 跳过等待选项 | MySQL 支持 NOWAIT 与 SKIP LOCKED,PostgreSQL 亦支持 |
| 适用平台 | MySQL 8.0 与 PostgreSQL 语法接近,锁强度细节不同 |
典型使用场景
- 任务队列取活:锁住一条待处理任务再改成处理中
- 库存扣减:先锁住库存行再判断与扣减
- 账户余额变更:避免并发同时读取同一余额
- 订单状态流转:保证同一订单不会被两个流程同时改
- 防止重复执行:锁住任务记录保证幂等
- 批量取数处理:用 SKIP LOCKED 让多个消费者各取各的
示例
示例
START TRANSACTION;
SELECT * FROM jobs
WHERE state = 'queued' # 条件要有索引,避免锁范围放大
ORDER BY id
LIMIT 1
FOR UPDATE; # 排他锁,锁住这一行
UPDATE jobs SET state = 'running', started_at = NOW()
WHERE id = 100;
COMMIT; # 提交后锁释放
-- 多消费者并发取活:跳过已被锁定的行
SELECT * FROM jobs
WHERE state = 'queued' ORDER BY id LIMIT 1
FOR UPDATE SKIP LOCKED;锁读选项对照
| 写法 | 锁类型 | 说明 |
|---|---|---|
| FOR UPDATE | 排他锁 | 其它事务读写都需等待 |
| FOR SHARE | 共享锁 | 多个读者可共存,写入需等待 |
| FOR UPDATE NOWAIT | 排他锁不等待 | 锁被占用立刻报错 |
| FOR UPDATE SKIP LOCKED | 排他锁跳过已锁定行 | 队列消费常用 |
| LOCK IN SHARE MODE | 旧共享锁写法 | MySQL 8.0 起改用 FOR SHARE |
锁读之后要在同一事务里完成修改并提交:漏掉 BEGIN 时锁在语句结束就释放,完全防不住并发。
WHERE 条件没有索引时锁定范围可能放大到扫描到的全部行,甚至近似全表;上线前先用执行计划确认走了索引。
SKIP LOCKED 是队列类场景的实用组合:多个消费者并发取任务时各取各的,不会互相等待。
使用要点
- 锁读与修改必须在同一事务内成对出现
- WHERE 条件要能走索引,避免锁范围放大
- 事务保持短:锁住到提交之间的耗时越短越好
- 队列场景用 SKIP LOCKED 提高并发消费效率
- 锁等待超时要有兜底:捕获超时错误并重试或降级
- 定期排查长事务与锁等待,及时发现争用热点