当前位置:首页 > 云谷精选

MySQL死锁怎么从日志里分析原因并避免

admin2026年03月18日云谷精选35.76万
摘要:# MySQL死锁:别慌,从日志里揪出“元凶”其实不难 我前两天刚处理完一个线上系统的死锁问题,那感觉,就像半夜两点被报警电话吵醒——血压瞬间就上来了。但说真的,死锁这事儿,在稍微有点规模的MySQL数据库里,简直跟感冒一样常见。很多开发一看到“Dead…

MySQL死锁:别慌,从日志里揪出“元凶”其实不难

我前两天刚处理完一个线上系统的死锁问题,那感觉,就像半夜两点被报警电话吵醒——血压瞬间就上来了。但说真的,死锁这事儿,在稍微有点规模的MySQL数据库里,简直跟感冒一样常见。很多开发一看到“Deadlock found when trying to get lock”就懵,要么重启大法,要么就想着“加机器、升配置”。

其实吧,绝大多数死锁,根源都在代码逻辑和事务设计上。今天我就跟你聊聊,怎么像老中医一样,从MySQL的日志里“望闻问切”,把死锁的原因给揪出来,并且告诉你以后怎么尽量躲开这些坑。

一、死锁日志:你的“案发现场”报告

首先,你得知道去哪儿找线索。MySQL很贴心,每次发生死锁,只要innodb_print_all_deadlocks这个参数是ON的(建议你打开),它就会在错误日志里给你留一份详细的“尸检报告”。

这份报告看着有点吓人,一堆LOCK WAITTRX ID,但其实核心就看几个地方。我直接拿一个真实的日志片段给你拆解:

LATEST DETECTED DEADLOCK
------------------------
2024-05-27 10:23:17 0x7f8e6c0b1700
*** (1) TRANSACTION:
TRANSACTION 123456789, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 100, OS thread handle 12345, query id 10001 10.0.0.1 app_user updating
UPDATE orders SET status = 'shipped' WHERE user_id = 100 AND order_id = 500;

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 100 page no 10 n bits 80 index idx_user_id of table `shop`.`orders` trx id 123456789 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 100 page no 20 n bits 80 index PRIMARY of table `shop`.`orders` trx id 123456789 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 10; compact format; info bits 0

*** (2) TRANSACTION:
TRANSACTION 987654321, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 101, OS thread handle 54321, query id 10002 10.0.0.2 app_user updating
UPDATE orders SET amount = amount + 50 WHERE order_id = 500;

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 100 page no 20 n bits 80 index PRIMARY of table `shop`.`orders` trx id 987654321 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 10; compact format; info bits 0

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 100 page no 10 n bits 80 index idx_user_id of table `shop`.`orders` trx id 987654321 lock_mode X locks rec but not gap waiting

看着眼晕?别急,我用人话给你翻译一下:

  1. 两个“凶手”(事务):事务1(TRX 123456789)和事务2(TRX 987654321)。
  2. 它们各自手里拿着什么(HOLDS THE LOCK)
    • 事务1:已经锁住了idx_user_id索引上user_id=100相关的记录。
    • 事务2:已经锁住了主键(PRIMARY)上order_id=500的记录。
  3. 它们各自在等什么(WAITING FOR)
    • 事务1:在眼巴巴地等主键上order_id=500的那把锁。
    • 事务2:在苦苦地等idx_user_id索引上user_id=100的那把锁。

这下明白了吧?经典的“抱死”场景:事务1拿着A锁等B锁,事务2拿着B锁等A锁,俩人谁都不撒手,系统一看,得,你俩“死锁”吧。

说白了,分析死锁日志,你就盯紧这个“谁拿了什么,又在等什么”的死循环。找到这个环,原因就找到了八成。

二、揪出元凶:常见死锁场景大起底

光看懂日志还不够,咱得知道这些“死循环”是怎么产生的。我总结了几种最常见、也最要命的场景,你看看是不是似曾相识。

场景1:不同顺序的更新操作

这是死锁界的“头号杀手”,上面那个日志例子就是典型。事务A先更新user_id=100的记录,再更新order_id=500的记录;事务B呢,正好反过来,先更新order_id=500,再更新user_id=100。在并发量一上来的时候,这种交叉等待几乎必然发生。

怎么破? 这是代码规范问题。团队内部必须强制约定,对同一批业务数据的更新,必须遵循相同的顺序。比如,都约定先按user_id过滤再按order_id处理。听起来简单,但很多团队就是没这个规矩,坑都是自己挖的。

场景2:Gap锁(间隙锁)的“幽灵”

这个有点隐蔽,但危害极大,尤其是在REPEATABLE-READ隔离级别下。比如你的事务执行了SELECT ... WHERE id > 100 FOR UPDATE,它不光锁住id>100的现有记录,还会锁住那个范围(一个“间隙”),防止其他事务在这个范围内插入。

这时候,如果另一个事务刚好想在这个间隙里插入一条新记录(比如INSERT ... id = 150),它就会被卡住。如果两个事务互相卡住了对方需要的间隙,死锁就又来了。这种死锁在日志里会看到lock_mode X locks gap before rec的字样。

怎么破? 首先评估一下,你的业务真的需要REPEATABLE-READ吗?很多业务用READ-COMMITTED完全够用,还能大幅减少间隙锁。如果非要用,那写FOR UPDATEUPDATE语句时,尽量让条件精准命中记录(用唯一键),别动不动就SELECT *然后FOR UPDATE一大片。

场景3:单表多行更新的“随机”锁

你以为一次更新很多行(UPDATE ... WHERE status = 'new')是一个原子操作?在InnoDB里,它其实是逐行加锁的。加锁的顺序,默认跟底层存储的物理顺序或者索引扫描顺序有关。这个顺序,你的代码控制不了

如果两个这样的事务并发,又各自持有了对方需要的某行锁,死锁就出现了。这种死锁在日志里可能看到多个Record lock

怎么破? 对于这种批量更新,如果业务允许,拆成多次、小批量的更新,并确保每次更新后事务尽快提交,减少锁的持有时间。或者,在应用层做排队,让同类更新串行执行。虽然损失了点并发,但换来了稳定。

三、避坑指南:从设计上远离死锁

分析是“治已病”,设计才是“治未病”。下面这几条,是我自己趟过坑之后总结的,你听听看有没有道理。

  1. 事务要短小精悍,快进快出 这是黄金法则。别在事务里做网络调用、别处理复杂业务逻辑、别让用户交互。事务的边界越清晰,锁持有的时间就越短,撞车的概率指数级下降。我见过最离谱的,一个事务里还调了第三方支付接口,不断超时重试,锁了快一分钟……你不死锁谁死锁?

  2. 访问顺序,团队要有“潜规则” 就像前面说的,更新多张表或者多个条件时,定个死规矩。比如,统一按“用户表 -> 订单表 -> 日志表”的顺序操作。把这个规矩写到代码规范里,新人来了先培训这个。

  3. 索引不是越多越好,要“刚刚好” 很多死锁是因为索引设计不当,导致加锁范围变大。比如,你有个非唯一索引,更新时可能就会产生间隙锁。定期用EXPLAIN看看你的慢查询和核心事务SQL,确保它们用上了最合适的索引,避免全表扫描(那会锁全表!)。

  4. 考虑降级隔离级别 真别把REPEATABLE-READ当默认宝贝。对于绝大多数互联网业务,READ-COMMITTED在数据一致性上完全够用,还能彻底避免间隙锁带来的各种奇葩死锁。改个配置,可能就解决了一大半问题。

  5. 准备好“后手”:重试机制 承认吧,在复杂的并发环境下,想完全杜绝死锁几乎不可能。所以,在应用层做一层优雅的重试就特别重要。捕获到死锁异常(MySQL会返回1213错误码)后,别慌,等个几百毫秒(随机退避一下),然后重试整个事务。大部分情况下,重试一两次就能成功。这招虽然土,但是真管用。

最后说两句

处理死锁,心态很重要。别一出问题就想着“是不是MySQL有bug”,或者“赶紧升级硬件压过去”。99%的死锁,都是人写出来的。 把它当成一个优化代码和架构的契机。

下次再看到死锁日志,别头大。就按我今天说的,先找到那个“等待环”,再对照常见场景对号入座,最后想想怎么从根上调整。多来几次,你就有感觉了。

行了,关于MySQL死锁,今天就聊这么多。如果你有更奇葩的死锁案例,或者有更好的解决思路,欢迎来聊聊——毕竟,踩坑的路上,多几个人做伴,总归是好的。

扫描二维码推送至手机访问。

版权声明:本文由www.ysyg.cn发布,如需转载请注明出处。

本文链接:http://www.ysyg.cn:80/?id=443

“MySQL死锁怎么从日志里分析原因并避免” 的相关文章

分析高防CDN的Cookie校验与重定向算法对CC肉鸡的自动清洗

# 当Cookie遇上“肉鸡”:高防CDN那点不为人知的清洗内幕 说实话,我这两年看过的站点防护配置,少说也有几百个了。最让我哭笑不得的不是那些裸奔的——人家至少心里有数。反而是那些上了“高防”还被打趴的,问题往往出在细节上,比如今天要聊的这个:**Co…

分析CDN高防中的动态反爬虫规则生成算法:对抗分布式采集

# CDN高防里的“捉虫”艺术:动态反爬算法如何让采集者空手而归 我前两天帮朋友看一个电商站点的日志,好家伙,一天之内来自两百多个不同IP的请求,访问路径整整齐齐,全是商品详情页,间隔时间精准得像秒表——这哪是正常用户,分明是开了分布式爬虫来“进货”的。…

探讨高防 CDN 应对协议混淆型攻击的流量特征匹配与拦截

# 当“伪装大师”遇上“火眼金睛”:聊聊高防CDN怎么揪出协议混淆攻击 前两天跟一个做游戏的朋友喝酒,他跟我大倒苦水:“你说我这游戏,上了高防CDN,平时DDoS、CC攻击都防得挺好。结果上个月,突然就卡了,后台一看流量也没爆,但玩家就是进不来,急得我直…

详解如何通过高防 CDN 日志定位攻击源 IP 及其所属僵尸网络特征

# 高防CDN日志里,藏着攻击者的“身份证” 前两天,一个做电商的朋友半夜给我打电话,语气都快急哭了:“流量又炸了,后台卡得一笔,高防CDN那边显示是‘已防护’,可我这业务还是半瘫。钱没少花,可攻击到底从哪来的?我总不能一直蒙在鼓里吧?” 这话我听着太…

探讨高防 CDN 应对大规模恶意爬虫抓取数据时的智能限速逻辑

# 别让爬虫拖垮你的服务器,聊聊高防CDN里那点“限速”的智慧 不知道你有没有过这种体验——半夜突然被运维的电话吵醒,说服务器CPU跑满了,网站慢得像蜗牛。一查日志,好家伙,全是某个IP段在疯狂请求你的商品页面,一秒钟几十次,跟不要钱似的。 这感觉,简…

探讨高防 CDN 应对利用真实用户浏览器发起的协同攻击防御方案

# 当攻击者不再用“机器人”:聊聊高防CDN怎么防住“真人浏览器”围攻 前两天,有个做电商的朋友半夜给我打电话,语气都快哭了:“流量看着都正常,用户也在点,可服务器就是崩了,这到底是人在访问还是鬼在访问?” 我让他把日志发我看看。好家伙,一眼就看出问题…