
MySQL 中的 FOR UPDATE 子句是一个强劲的工具,用于在事务中实现行级锁(在可能的情况下),它属于悲观锁策略的一部分。核心目的是确保在你读取某些行之后、完成修改并提交事务之前,其他并发事务不能修改或锁定这些行,从而保证数据的一致性。
FOR UPDATE 的基本用法
FOR UPDATE 一般与 SELECT 语句结合使用,在事务中锁定查询到的行,防止其他事务对这些行进行修改或删除,直到当前事务提交或回滚。
START TRANSACTION;
SELECT * FROM your_table
WHERE your_conditions
FOR UPDATE; — 锁定匹配条件的行
— … 在这里执行基于查询结果的计算或修改操作 (UPDATE, DELETE, INSERT) …
COMMIT; — 提交事务,释放所有锁
假设需要修改用户余额:
START TRANSACTION;
— 锁定 ID=1 的用户行
SELECT balance FROM users WHERE id = 1 FOR UPDATE;
— 执行修改(例如扣除 100 元)
UPDATE users SET balance = balance – 100 WHERE id = 1;
COMMIT; — 提交后释放锁
作用
- 防止脏写:确保当前事务修改的数据不会被其他事务同时修改,避免数据不一致。
- 实现悲观锁:在读取数据时就锁定行,适合并发冲突频繁的场景。
注意事项
- 必须在事务中使用
FOR UPDATE 仅在事务上下文中生效!如果在自动提交模式下(autocommit=1),单条 SELECT … FOR UPDATE 会被视为一个立即提交的事务,锁会获取后立即释放,失去了意义。
- 明确事务边界和及时提交
事务中 SELECT … FOR UPDATE 后应尽快完成业务逻辑并提交/回滚。长时间持有锁会导致严重性能问题(其他事务排队等待)。
无论成功或失败,事务必通过 COMMIT 或 ROLLBACK 明确结束事务,释放锁。应用崩溃或连接异常中断可能导致锁未释放(依赖 MySQL 超时机制)。
- 索引是行锁的关键
确保查询条件 WHERE 子句使用了合适的索引。 不使用索引会导致全表扫描,很可能升级为表级锁,严重阻塞整个表的并发操作。检查 EXPLAIN 输出,确认使用了索引扫描。
- 范围条件与间隙锁
如果你的查询使用范围条件(如 WHERE id > 100 AND id < 200),InnoDB 不仅会锁定存在的行(100 到 200 之间),一般还会锁定一个“间隙”,阻止其他事务在这个范围内插入新行(防止幻读)。
- 死锁风险
多个事务按照不同顺序加锁时可能发生死锁(如事务A锁行1等行2,事务B锁行2等行1)。InnoDB 有死锁检测机制,一般会自动回滚其中一个事务(牺牲者)。
尽量让所有并发事务按一样的顺序访问和锁定表行(例如,按主键ID排序处理)。在应用层处理死锁异常(重试机制)。
- 锁定范围 vs 过滤条件
FOR UPDATE 是在满足过滤条件的行被找到后,才应用锁。它不会锁定扫描过程中跳过的不符合条件的行(除非需要间隙锁)。
- 兼容性问题
FOR UPDATE 锁与普通的 SELECT(非锁定读)兼容。只读查询不会被阻塞。FOR UPDATE 锁与其他 FOR UPDATE 锁、FOR SHARE 锁、UPDATE/DELETE 操作冲突。
- 超时与连接池
使用 innodb_lock_wait_timeout 设置锁等待超时时间(默认 50 秒),避免无限等待。如果应用错误地未结束事务就归还连接,下一个用户复用该连接时会继承未提交的事务及其锁!会导致极其诡异的并发问题。
- 替代方案:
乐观锁:在冲突较少发生的场景,乐观锁(如使用 version 字段或时间戳)往往是更好的选择。它通过检查在读取数据后是否被修改来决定是否更新,避免了数据库加锁的开销。FOR UPDATE 是悲观锁。
常见场景
- 余额更新/扣款: 先查询当前余额(加锁),判断是否充足,然后减去金额更新余额。
- 库存扣减: 避免超卖的关键。查询库存(加锁),判断 > 0,扣减库存后更新。
- 防止并发修改冲突: 在基于查询结果进行后续复杂计算并更新时,确保计算依据的数据在过程中不会被其他事务篡改。
- 完整性检查: 在插入依赖某行记录的新记录之前,锁定该行,确保它在插入过程中不会被删除。



