mysql悲观锁卡住请求本质是行锁等待堆积,因select...for update在高并发下事务持有锁时间过长、未走索引或未及时提交导致;需优化索引、缩短事务、结合redis缓存与乐观锁分层协作。

MySQL 悲观锁卡住请求,本质是行锁等待堆积
当 SELECT ... FOR UPDATE 在高并发下频繁执行,且事务持有锁时间稍长(比如后续有 HTTP 调用、日志写入或复杂计算),就会导致其他事务在 innodb_row_lock_waits 指标上飙升,SHOW ENGINE INNODB STATUS 里能看到大量 LOCK WAIT 状态。这不是数据库扛不住,而是锁粒度和持有时间没对齐业务节奏。
常见误操作:FOR UPDATE 包裹了非必要逻辑;事务未显式 COMMIT 或 ROLLBACK;WHERE 条件没走索引,升级为表锁。
- 检查锁等待:监控
Innodb_row_lock_waits和Innodb_row_lock_time_avg - 确认是否走索引:EXPLAIN
SELECT ... FOR UPDATE WHERE id = ?,确保type是const或ref - 缩短事务:把外部 I/O、JSON 解析等移出事务块,只留 DB 读写
乐观锁 + Redis 缓存不是二选一,而是分层协作
直接把 SELECT ... FOR UPDATE 换成 UPDATE ... WHERE version = ? 并不能解决热点更新冲突——失败重试本身会放大延迟,且没解决「读多写少」场景下的 DB 压力。Redis 的作用不是替代锁,而是把「可缓存的读」和「幂等的写校验」解耦。
典型结构:GET stock:1001 → 命中则直接返回;未命中则查 DB + SET stock:1001 {json} EX 60;写操作先 GET stock:1001 解析当前 version 和 quantity,再发带 WHERE id = 1001 AND version = ? 的 UPDATE,成功后 DEL stock:1001 触发下次读回源。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- Redis key 命名必须含业务主键,避免缓存污染,如
stock:{item_id} - 写操作失败时,不要无脑重试,应
GET新值判断是否真冲突(比如别人已改库存为 0,你再扣就是超卖) - 设置缓存过期时间(EX)要略大于 DB 事务平均耗时,防止缓存击穿时 DB 瞬间被打垮
Redis 和 MySQL 数据不一致的修复点在哪
缓存和 DB 不一致不是“要不要加延时双删”,而是看哪一层承担最终一致性保障。关键在于:写操作必须原子性地完成「DB 更新 + 缓存失效」,且失败要有补偿路径。
推荐做法:DB 更新成功后,同步执行 DEL stock:1001;如果 DEL 失败(网络抖动、Redis 挂了),靠定时任务扫描 DB 变更表(或监听 binlog)异步清理缓存。不要用「先删缓存再更新 DB」,因为并发读可能把旧值重写进缓存。
- 删除缓存失败必须记录日志,至少包含 key、失败时间、DB 主键 ID
- binlog 解析方案可用 Maxwell 或 Debezium,但中小系统用定时任务查
updated_at > ?更可控 - 避免用 SET 替代 DEL:写新值进缓存容易掩盖脏数据,尤其是字段部分更新时
什么时候不该切乐观锁 + Redis
如果业务要求强一致性(比如银行转账余额变更)、或单条记录 QPS 极低(FOR UPDATE 在这些场景仍是合理选择。
另一个隐形陷阱:Redis 作为缓存层引入了新故障点。如果 Redis 集群不可用,所有读请求将穿透到 DB,若没做降级(如本地 Caffeine 缓存兜底或请求限流),DB 会雪崩。
- 上线前压测 Redis 不可用时的 DB 承载能力,确认连接数、慢查询、CPU 是否在阈值内
- 对库存类关键字段,仍需在 MySQL 层加 CHECK CONSTRAINT 或 BEFORE UPDATE TRIGGER 做兜底校验
- 乐观锁的 version 字段别用 INT 自增,建议用
BIGINT UNSIGNED+DEFAULT 0,避免溢出










