优先加索引→控制事务粒度→再考虑重试;用主键更新替代非索引字段更新,避免间隙锁;乐观锁适用于低冲突场景并需限制重试次数,悲观锁需确保走索引;pg无间隙锁,迁移时需调整并发策略。

UPDATE 时出现 “Deadlock found when trying to get lock” 怎么办
MySQL 在高并发下执行 UPDATE 容易触发死锁,不是代码写错了,而是事务加锁顺序不一致导致的。InnoDB 默认用行级锁,但若扫描范围大(比如没走索引),会升级成间隙锁或临键锁,多个事务交叉等待就卡住。
- 死锁日志里通常能看到两个事务各自持有某行锁、又在等对方释放另一行锁
- 常见于按非主键条件更新(如
UPDATE users SET status = 1 WHERE email = 'x@y.z'),而email没建索引 - 解决优先级:先加索引 → 再控制事务粒度 → 最后才考虑重试逻辑
别一上来就加 SELECT ... FOR UPDATE 或改隔离级别,90% 的死锁源于缺失索引或 SQL 扫描太宽。
如何让并发 UPDATE 变成串行但不拖慢性能
核心思路是把“逻辑上要争抢同一资源”的操作,映射到数据库里同一个索引键上,让 InnoDB 自动串行化,而不是靠应用层加分布式锁。
- 确保所有并发更新都命中同一索引的唯一值,比如用
user_id(主键)代替username(可能重复或无索引) - 如果业务必须按非唯一字段更新(如订单号前缀),可加一层映射表,把前缀哈希后存为固定长度字段并建唯一索引
- 避免在事务里做 HTTP 调用、文件读写等长耗时操作,锁持有时间越短越好
-
innodb_lock_wait_timeout默认 50 秒,线上建议调成 5–10 秒,让失败更快暴露
示例:更新用户余额时,不要 UPDATE accounts SET balance = balance + ? WHERE uid IN (SELECT uid FROM orders WHERE order_no = ?),改成先查出 uid,再用主键更新——减少锁范围且可控。
乐观锁 vs 悲观锁,什么时候该用哪个
乐观锁适合冲突概率低、写操作轻量的场景(如点赞数+1);悲观锁适合冲突频繁、业务逻辑强依赖“读-改-写”原子性的场景(如库存扣减)。
- 乐观锁典型做法:加
version字段或用WHERE balance = ?校验旧值,UPDATE返回影响行数为 0 就重试 - 悲观锁用
SELECT ... FOR UPDATE,但必须在事务内、且语句能走索引,否则会锁全表 - 注意:MySQL 的
FOR UPDATE在READ COMMITTED下只锁命中的行,在REPEATABLE READ下还会锁间隙,容易扩大影响面 - 不要用
SELECT ... LOCK IN SHARE MODE做写保护,它不阻止其他事务的UPDATE
一个容易被忽略的点:乐观锁重试不能无限循环,尤其在高并发下,要加退避(如指数回退)和最大重试次数,否则可能把数据库打挂。
PostgreSQL 和 MySQL 处理并发 UPDATE 的关键差异
两者底层锁机制不同,直接套用 MySQL 经验在 PG 上可能翻车。
- PostgreSQL 没有间隙锁,所以不会因为范围查询产生“幻读锁”,但有
SELECT ... FOR UPDATE和FOR NO KEY UPDATE区分 - PG 的
UPDATE默认自带行级锁,即使不显式加FOR UPDATE,只要事务未提交,其他事务更新同一行就会阻塞 - MySQL 的
UPDATE若没走索引,可能锁表;PG 则始终只锁实际更新的行,但代价是可能触发更多的 tuple version 冲突(即“tuple concurrently updated”错误) - PG 推荐用
INSERT ... ON CONFLICT DO UPDATE替代先查后更,避免竞态,MySQL 8.0+ 也支持类似语法INSERT ... ON DUPLICATE KEY UPDATE
真正麻烦的是跨数据库迁移时,把 MySQL 的间隙锁预期直接搬到 PG,结果发现原来防住的并发问题又回来了——因为 PG 根本不锁间隙。










