悲观锁用select...for update配合显式事务,乐观锁靠version字段+where条件更新;两者不能混用,选错场景反而放大问题。因为mysql默认autocommit=1,非事务中for update执行完即提交,锁瞬时释放,等同无效;必须begin开始、commit/rollback结束事务,且查询须命中索引才能行锁,否则升级表锁。

直接说结论:悲观锁用 SELECT ... FOR UPDATE 配合显式事务,乐观锁靠 version 字段 + WHERE 条件更新;两者不能混用,选错场景反而放大问题。
为什么 SELECT ... FOR UPDATE 在非事务中完全无效
很多人写了 SELECT * FROM accounts WHERE id = 1 FOR UPDATE 却发现没锁住,根本原因是 MySQL 默认开启 autocommit=1。语句执行完立刻提交,锁也随即释放——等于没锁。
必须显式控制事务生命周期:
- 开头加
BEGIN或START TRANSACTION - 所有操作(查询、判断、更新)必须在同一个事务块内
- 结尾用
COMMIT或ROLLBACK显式结束
在 GORM 中对应的是 db.Begin() + tx.Set("gorm:query_option", "for update"),漏掉任意一环都会退化为普通查询。
FOR UPDATE 到底锁哪一行?关键看查询条件是否命中索引
锁的粒度不是由语句决定的,而是由执行计划决定的。InnoDB 只有在能通过索引精确定位记录时才加行锁;否则升级为表锁,瞬间拖垮并发能力。
常见陷阱:
- 用
WHERE name = 'xxx',但name没建索引 → 锁整张表 - 用
WHERE id > 100范围查询 → 触发间隙锁(Gap Lock),可能锁住不存在的记录区间 - 用
WHERE JSON_EXTRACT(data, '$.status') = 1→ 无法走索引,大概率表锁
验证方法:执行 EXPLAIN FORMAT=TREE 看是否显示 index_condition,或查 information_schema.INNODB_TRX 确认锁类型。
乐观锁的 UPDATE ... WHERE version = ? 为什么有时“失效”
表面上看,UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = 5 返回影响行为 0 就代表冲突,但实际常踩三个坑:
- 业务层没检查
RowsAffected,直接当成功处理 -
version字段未设为NOT NULL,空值参与=判断永远为UNKNOWN,导致条件恒假 - 多个字段同时更新(如
stock和status),但只用一个version控制,掩盖部分字段被绕过更新的风险
更稳妥的做法是把关键业务字段也纳入条件,比如 AND stock >= 1,避免靠 version 单点校验兜底。
高并发下两种锁的真实代价差异
悲观锁的阻塞是明确的:请求排队、超时、连接堆积;乐观锁的失败是隐性的:重试逻辑写不全、CPU 白耗、日志里全是“更新失败”,但监控看不出瓶颈在哪。
真正容易被忽略的是——
- 悲观锁在长事务中会持有锁数秒甚至分钟,一旦下游服务响应慢,整个数据库连接池可能被占满
- 乐观锁在冲突率 > 5% 时,平均重试 2–3 次,实际 QPS 折损严重,且重试时机若没加退避(如指数等待),会加剧冲突
没有银弹。库存扣减这类强一致性场景,宁可调优悲观锁+缩短事务链路;用户资料修改这类低冲突操作,用乐观锁更轻量——但得真测出冲突率,别靠猜。











