SELECT ... FOR UPDATE 必须在显式事务(BEGIN/START TRANSACTION)中使用,否则无效;需确保WHERE条件走索引避免表锁;UPDATE未匹配行时锁不释放;Redis校验须在加锁后进行,不可替代MySQL一致性。

select for update 在事务里没生效?检查是否漏了 BEGIN
MySQL 的 SELECT ... FOR UPDATE 只在事务中起作用,单独执行等同于普通查询。常见现象是:扣减库存时并发请求仍超卖,查日志发现锁根本没加。
- 必须显式开启事务:
BEGIN或START TRANSACTION,不能依赖自动提交(autocommit=1) - 执行完
SELECT ... FOR UPDATE后,要接着做UPDATE和COMMIT,否则锁会等到事务超时才释放 - 如果用 ORM(如 Django、MyBatis),确认它没把语句拆成多个独立语句——有些框架默认每条 SQL 自动 commit
WHERE 条件没走索引?锁会升级成表锁
SELECT ... FOR UPDATE 加的是行锁,但前提是 WHERE 匹配的列有可用索引。否则 MySQL 会退化为锁全表,性能断崖下跌,还可能引发死锁。
- 用
EXPLAIN看执行计划,确认type是ref或const,且key显示用了哪个索引 - 主键或唯一索引最安全;普通索引要注意:如果 WHERE 条件包含非索引字段(如
WHERE status=1 AND sku_id=100),而status没索引,也可能失效 - 注意隐式类型转换:比如
sku_id是VARCHAR,但传了数字 100,MySQL 会放弃索引
UPDATE 语句没匹配到行?锁不会释放,还会阻塞后续请求
这是最容易被忽略的坑:SELECT ... FOR UPDATE 成功加锁,但紧接着的 UPDATE 因条件不满足没更新任何行,事务仍开着,锁一直占着。
- 务必检查
UPDATE的影响行数(如 MySQL 返回的Rows matched: 1 Changed: 1),不是只看是否报错 - 推荐在事务内先查库存值,再判断是否足够,不够就直接
ROLLBACK,别让锁空挂着 - 应用层要有超时控制:比如设置事务最长 3 秒,避免一个卡住的请求拖垮整个库存服务
和 Redis 库存比对时顺序错了?脏读风险藏得很深
很多系统用 Redis 做前置校验(“还有库存吗?”),再进 MySQL 扣减。但如果先查 Redis、再 SELECT ... FOR UPDATE,中间窗口期可能被其他事务改掉数据。
- 正确顺序是:先
SELECT ... FOR UPDATE锁住 MySQL 行,再读取当前库存值,最后跟业务逻辑比对(比如是否 ≥ 要扣的数量) - Redis 只能当缓存或异步落库用,不能替代 MySQL 的一致性校验
- 如果真要用双写,至少保证 MySQL 是权威源,Redis 更新必须在
COMMIT后触发(例如通过 binlog 或应用层 post-commit hook)
SELECT ... FOR UPDATE 的边界很窄:它只保单行强一致,不保业务语义(比如“限购 2 件”得靠应用层计数),也不保跨表、跨库场景。一不留神,锁就变成瓶颈。


















