MySQL行级锁仅在InnoDB引擎且WHERE条件命中索引(主键/唯一/普通索引)时生效;否则退化为表级锁或锁住全部索引记录及间隙,导致并发下降。

MySQL 行级锁在什么情况下真正生效
行级锁只在 InnoDB 引擎下有效,且必须走索引(主键或唯一/普通索引)才能锁定具体行;全表扫描或未命中索引时会退化为表级锁(或锁住所有索引记录,甚至间隙),导致并发性能骤降。
- 使用
SELECT ... FOR UPDATE或UPDATE/DELETE语句时,若WHERE条件无索引,InnoDB 可能对聚簇索引的每条记录加锁,等效于锁表 - 即使有索引,若查询条件是范围(如
id > 100),InnoDB 会加间隙锁(Gap Lock)或临键锁(Next-Key Lock),锁住不存在的“间隙”,防止幻读——这也常被误认为“锁了不该锁的行” - 注意
UPDATE t SET x=1 WHERE y=2:如果y列无索引,该语句可能锁住整张表的聚簇索引所有行
死锁是如何被 MySQL 自动检测并终止的
MySQL 通过 waits-for graph(等待图)实时检测循环等待关系。一旦发现两个及以上事务互相持有对方需要的锁、且都在等待对方释放,就立即选一个事务作为牺牲者(victim),回滚它并抛出错误:Deadlock found when trying to get lock; try restarting transaction。
- 被选为牺牲者的事务不一定是执行时间长的那个,而是代价最小的(例如修改行数少、undo log 小)
- 死锁检测本身有开销,默认开启(
innodb_deadlock_detect=ON),高并发更新场景可考虑关闭,但需配合应用层重试逻辑 - 死锁日志不会自动写入 error log,需手动执行
SHOW ENGINE INNODB STATUS\G查看最近一次死锁详情,重点关注TRANSACTIONS部分中的WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)
避免死锁的四个关键实操原则
死锁无法完全杜绝,但可通过约束访问模式大幅降低概率。核心是让多个事务以相同顺序获取锁。
-
按主键顺序更新:统一先查再按
ORDER BY primary_key排序后批量更新,避免 A 更新 (1,3)、B 更新 (3,1) 这类交叉 - 缩小事务粒度:把大事务拆成多个小事务,减少锁持有时间;尤其避免在事务中做 RPC、文件读写、sleep 等外部耗时操作
-
一致的索引访问路径:确保不同业务模块更新同一张表时,WHERE 条件始终走同一个索引(例如都用
user_id而非有时用email有时用phone) -
显式加锁 + 超时控制:用
SELECT ... FOR UPDATE WAIT 5(MySQL 8.0.1+)代替无超时的阻塞等待,避免长时间挂起
死锁发生后应用层该怎么安全重试
不能简单捕获异常后无脑重试,否则可能无限循环触发死锁;必须结合业务语义判断是否可重试,并限制次数。
try:
with connection.cursor() as cursor:
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE user_id = %s", [uid])
cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE user_id = %s", [target_uid])
connection.commit()
except pymysql.err.IntegrityError as e:
if "Deadlock found" in str(e):
# 记录日志,等待随机抖动后重试(避免重试洪峰)
time.sleep(random.uniform(0.01, 0.1))
retry_count += 1
if retry_count <= 3:
continue
else:
raise # 放弃重试,交由上层处理
else:
raise注意:重试前最好重新 SELECT 当前值,确认业务状态未被其他事务改变(尤其是余额、库存等敏感字段),否则可能出现超扣或重复加款。
死锁最隐蔽的来源往往不是 SQL 写法,而是事务边界不清、ORM 自动生成的 N+1 查询、或者触发器隐式开启新子事务——这些地方最容易漏掉锁分析。










