mysql并发场景中for update如何使用_mysql行锁实践

来源:这里教程网 时间:2026-02-28 20:48:47 作者:

for update 在 MySQL 中到底锁什么

FOR UPDATE
不是锁整张表,也不是锁索引范围,而是对 当前 SELECT 扫描到的、满足 WHERE 条件的行(且有对应索引支持)加排他行锁(X 锁)。前提是事务隔离级别为
REPEATABLE READ
READ COMMITTED
,且查询走的是**唯一索引或普通索引(非全表扫描)**。

如果

WHERE
条件不走索引,MySQL 会退化为锁所有扫描过的记录(甚至可能升级为间隙锁+临键锁组合),极端情况下等效于表级锁定,严重拖慢并发。

有主键或唯一索引:只锁匹配的那 1 行 有普通索引但非唯一:锁匹配的行 + 对应的间隙(防幻读) 没索引 or 索引失效(如
WHERE name LIKE '%abc'
):可能锁全表扫描涉及的所有行,风险极高

典型误用:在非事务块里执行 for update

SELECT ... FOR UPDATE
必须运行在显式事务中,否则语句执行完就自动提交,锁立刻释放 —— 这会让“先查后更”的逻辑彻底失效。

START TRANSACTION;
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE;
-- 此时其他事务对 id=123 的 UPDATE/SELECT FOR UPDATE 会被阻塞
UPDATE accounts SET balance = balance - 100 WHERE id = 123;
COMMIT; -- 锁在此刻才释放

常见错误写法:

SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; -- 没 START TRANSACTION!锁秒放
UPDATE accounts SET balance = balance - 100 WHERE id = 123; -- 此时已无锁保护,超扣风险

死锁是怎么被 for update 招来的

两个事务按不同顺序对同一组行加

FOR UPDATE
,就极易触发死锁。MySQL 检测到后会回滚其中一个事务,并报错:
Deadlock found when trying to get lock; try restarting transaction

事务 A:先
SELECT ... WHERE id = 100 FOR UPDATE
,再
SELECT ... WHERE id = 200 FOR UPDATE
事务 B:先
SELECT ... WHERE id = 200 FOR UPDATE
,再
SELECT ... WHERE id = 100 FOR UPDATE

解决办法只有两个:

所有业务路径统一按相同顺序(如总是按
id ASC
)获取行锁
捕获死锁异常,在应用层重试整个事务(注意幂等性)

替代方案:用 select for update skip locked 避免排队

当多个线程争抢一批任务(如订单处理队列),传统

FOR UPDATE
会让后到者一直等待,响应变慢。MySQL 8.0+ 支持
SKIP LOCKED
,跳过已被锁的行,直接拿下一个可用行:

START TRANSACTION;
SELECT * FROM tasks 
  WHERE status = 'pending' 
  ORDER BY id 
  LIMIT 1 
  FOR UPDATE SKIP LOCKED;
-- 即使其他事务已锁住前几行,这条语句也不会阻塞,而是返回下一个未被锁的 pending 任务
UPDATE tasks SET status = 'processing' WHERE id = ?;
COMMIT;

注意:

SKIP LOCKED
仅适用于
READ COMMITTED
隔离级别下效果最稳定;
REPEATABLE READ
下某些场景可能行为不一致,需实测验证。

真正难的不是写对这句 SQL,而是想清楚「哪些数据必须强一致性」和「哪些可以接受短暂不一致」——锁是代价,不是装饰。

相关推荐