根本原因是锁粒度与事务时长不匹配:无索引导致表锁、慢查询延长持锁时间、事务混入非db操作;应优化执行计划、缩小锁范围、缩短事务、避免隐式锁升级。

为什么 SELECT ... FOR UPDATE 在高并发下会卡死
根本原因不是锁本身,而是锁的粒度和事务持续时间不匹配。InnoDB 默认走行锁,但若查询条件没走索引,会退化成表锁;更常见的是事务里混了慢查询、网络 IO 或应用层逻辑,让锁持有时间远超预期。
- 检查执行计划:务必确认
EXPLAIN输出中type是ref或range,不是ALL或index - 锁范围要最小化:避免
SELECT * FROM t WHERE status = 'pending' FOR UPDATE这种无主键/唯一约束的扫描 - 事务越短越好:把非数据库操作(如 HTTP 调用、JSON 解析)全移出事务块,只留真正需要原子性的语句
- 注意隐式锁升级:MySQL 8.0+ 中,当单个事务锁住超过 1000 行,默认可能触发锁升级警告(虽不强制,但会显著拖慢)
如何安全地做“先查后更新”而不丢数据
这是高并发扣减库存、抢优惠券最典型的幻读/超卖场景。UPDATE ... WHERE 原子判断比“查→判→更”三步走可靠得多,但前提是 where 条件能精确命中且包含业务约束。
- 别依赖应用层判断余额:
if (balance > amount) { UPDATE ... }必然超卖 - 用带校验的单条
UPDATE:例如UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100,然后检查ROW_COUNT()是否为 1 - 如果业务逻辑复杂(比如要同时更新多个表),改用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO配合唯一索引兜底 - 注意:MySQL 的
READ COMMITTED隔离级别下,SELECT ... FOR UPDATE不会阻塞快照读,但UPDATE仍会加锁——别误以为换隔离级别就能绕过锁
innodb_lock_wait_timeout 设太小反而更糟
默认 50 秒看似很长,但线上服务常设成 3~5 秒来“快速失败”。问题在于:它只控制等待锁的时间,不控制锁本身持有时间。设太小会导致大量事务被杀,重试风暴反而推高整体锁冲突率。
系统功能强大、操作便捷并具有高度延续开发的内容与知识管理系统,并可集合系统强大的新闻、产品、下载、人才、留言、搜索引擎优化、等功能模块,为企业部门提供一个简单、易用、开放、可扩展的企业信息门户平台或电子商务运行平台。开发人员为脆弱页面专门设计了防刷新系统,自动阻止恶意访问和攻击;安全检查应用于每一处代码中,每个提交到系统查询语句中的变量都经过过滤,可自动屏蔽恶意攻击代码,从而全面防止SQL注入攻击
- 先看真实锁等待分布:
SELECT * FROM performance_schema.data_lock_waits(MySQL 8.0+)或解析SHOW ENGINE INNODB STATUS中的TRANSACTIONS段 - 调优方向是缩短锁持有时间,不是压缩等待时间。比如把一个 2 秒的事务拆成两个 300ms 事务,比把 timeout 从 50 改成 1 秒有效得多
- 应用层需区分错误类型:捕获
Lock wait timeout exceeded后,不能无脑重试,得结合业务判断是否可降级(如“库存暂不可用”而非“下单失败”)
连接池 + 事务边界不一致引发的隐形死锁
很多框架(Spring Boot、Laravel)默认开启事务代理,但开发者手动在 DAO 层又开了一次事务,或者在异步线程里复用主线程连接——此时连接池里的连接状态和事务实际状态脱节,FOR UPDATE 锁可能一直挂着不释放。
- 确认事务是否真提交:
SELECT trx_id, trx_state, trx_started FROM information_schema.INNODB_TRX,长期trx_state = RUNNING且无对应应用请求,大概率是连接没正确归还池 - 禁用自动事务传播:Spring 中明确标注
@Transactional(propagation = Propagation.REQUIRED),避免NESTED或REQUIRES_NEW在高频路径滥用 - 连接池配置必须匹配事务预期:HikariCP 的
connection-timeout应大于最大事务耗时,leak-detection-threshold开启后能抓到未关闭的事务
真正的瓶颈往往不在 SQL 写法,而在事务生命周期和连接状态的错配。只要有一个连接在事务中睡着了,整个资源池就可能被它拖垮。









