🔴 数据库锁冲突:抢座位吵架

1205 = 等太久放弃,1213 = 互相等强滚一个;这两张图是 L-21 锁专项修复留下的“复发哨兵”。覆盖核心盘 🔴 组 2 张面板(c34–c35)· 后端核心仪表盘 siliang-backend。

事务 A · 删画布输出 operation=workspace_output_delete 重试兜底每触发一次 → 计数 +1 事务 B · 节点换源 operation=workspace_node_edge_replace 重试兜底每触发一次 → 计数 +1 同一行数据(画布节点) 行锁:谁拿到谁先改 从拿锁到放锁 = 锁窗口 锁窗口越长,外面排队越长 出路一 · 1205 锁等待超时 排队等满时限,主动放弃 温和失败:通常触发重试兜底 MySQL 默认约等 50 秒(通用默认值) ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 出路二 · 1213 死锁 俩事务互相等,数据库强滚一个 行1·A持 行2·B持 A 想要 B 想要 裁判(数据库)把一个事务回滚 ERROR 1213 (HY000): Deadlock found when trying to get lock; try restarting L-21 修复后 · 哨兵恒 0(绿) 近 1h 计数 < 1:锁问题修干净了 increase(siliang_db_lock_errors_total[1h]) 复发警报 · ≥1 黄 / ≥10 红 持续 > 0 = 仍有锁窗口未收敛 → 去速率图定位 sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code) 想改这行 → 排队拿锁 也想改 → 排队 等太久 撞死结 每次兜底重试都计数 强滚回滚也计数 事务 / 健康 两条出路都是事故 数据与行锁 哨兵思想:修复后恒 0 = 防回退警报器(非零即违规)

💡 一句话理解

数据库改数据前要先给那几行上锁(像进占座自习室把包放椅子上)。两个事务抢同一行,就只有两条出路:等太久主动放弃(1205 锁等待超时),或者互相等对方手里的锁、被数据库强行拖走一个(1213 死锁)。两败俱伤,没有赢家。

这两张面板是锁专项修复(L-21)留下的哨兵:根因修净后近 1h 计数应恒为 0;再冒出来,就说明有新的锁窗口在挡路——它回答的问题始终是“锁问题修干净了吗?有没有复发?”

🧩 费曼拆解 讲给完全没接触过的小白

占座的故事。把一行数据想成图书馆的一张桌子。事务 A 坐下来,把包放在椅子上(加锁),开始改桌上的东西。事务 B 想坐,发现被占,只能站在旁边等(锁等待)。从 A 落座到起身离开的这段时间,就叫锁窗口——窗口越长,外面排队的人越多,队伍能排到街口。

两条出路,都是坏结局。出路一:B 等满规定的时限(MySQL 默认约等 50 秒的锁等待超时)还没等到,主动放弃走人——这是 1205,温和失败,没有互害,通常触发重试兜底。出路二:更糟的剧本——A 占了 1 号桌想要 2 号桌,B 占了 2 号桌想要 1 号桌,俩人互相等,谁都动不了。管理员(数据库)发现这个死结,直接把其中一个请走(回滚一个,1213)。被拖走的那个已经干了一半活,回滚有成本,所以 1213 比 1205 更恶劣。

哨兵。L-21 锁专项修复把“删画布输出 / 节点换源”这两类操作的锁窗口缩短了,并且给它们的重试兜底装了计数器:兜底每触发一次就 +1(记在 siliang_db_lock_errors_total)。修复上线后,近 1h 计数恒 0 就是绿灯;哪天它再亮起来,就说明有新的锁窗口在挡路——这就是“防回退哨兵”的思想。

类比里的东西系统里对应的东西
图书馆的桌子一行数据(如画布节点行)。事务改它之前必须先拿行锁,保证数据不写坏。
占座的包行锁。从拿到锁到放锁的时间 = 锁窗口;窗口越长,别的事务等得越久。
站着等的人被阻塞的其他事务。它们占着数据库连接干等,排队的队伍越排越长。
等满时限走人1205 锁等待超时——温和失败,这一个操作放弃,通常触发重试兜底。
互相等被管理员拖走1213 死锁——数据库发现死结,强制回滚其中一个事务,让另一个通过。
吵架登记本siliang_db_lock_errors_total 计数器:重试兜底每触发一次 +1,近 1h 次数就是哨兵读数。

🛠 动手验证(在 Grafana 里亲手做一遍)

  1. 打开 c34 大数字「数据库锁冲突(近 1h 次数)」——修复后应恒 0(<1 绿);≥1 黄 / ≥10 红,先记住这个读数的量级。
  2. 看 c35 速率图(按 operation × code 拆)——确认是删画布输出还是节点换源、是 1205 还是 1213:哪类操作在跟谁抢锁,一眼定位。
  3. 把时间轴拉到发布窗口附近——新冒出来的锁窗口常伴随发布(新代码引入长事务);对照发布记录看第一次非零出现的时间。
  4. 冲高时刻对照 HTTP 5xx(c17)——锁冲突通常连带一波接口 500:时间戳对得上,就是它连坐的。

🧠 必知必会 看懂本组 2 张图的地基

行锁与锁窗口
锁本身不是坏事,是保证数据不写坏的必需品。出问题的是锁窗口失控:事务里混入慢查询、外部调用,窗口被拉长几百倍,锁等待扩散成事故。
# 锁窗口 = 从拿到锁到放锁的时间
# 窗口里混入慢查询/外部调用 → 排队雪崩
1205 锁等待超时
排队等锁,等太久,主动放弃——“我再等 50 秒,等不到就走人”(MySQL 默认约 50 秒,通用默认值)。温和失败:没有互害,只是这一个操作失败,通常触发重试。
ERROR 1205 (HY000): Lock wait timeout exceeded;
  try restarting transaction
# 等满时限放弃,事务本身没互害
1213 死锁
两个事务互相等对方手里的锁,谁都动不了——数据库发现死结,强制回滚其中一个。比 1205 恶劣:被牺牲的事务已经干了一半活,回滚有成本。
ERROR 1213 (HY000): Deadlock found when trying
  to get lock; try restarting transaction
# 预防:大家按同样顺序拿锁
哨兵思想
历史修复加“防回退警报器”:恒 0 是预期,非零即违规。c34 就是 L-21 修复的哨兵——它存在的意义是“复发第一时间知道”,而不是“平时好看”。
sum(increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]))
# 修复后恒 0;≥1 黄 / ≥10 红
increase vs rate
“近 1h 总共几次”用 increase(窗口内累计涨幅);“每秒几次、按什么维度拆”用 rate。大数字和速率图各司其职。
sum(increase(siliang_db_lock_errors_total[1h]))  # → 近 1h 总共几次
sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)  # → 速率细看
label 交叉定位
operation(删画布输出 / 节点换源)× code(1205 / 1213)两个维度交叉,直接回答“哪类操作在用哪种死法抢锁”——这是修复定位的第一入口。
sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)
# 哪类操作 × 哪种死法,一眼定位
锁冲突的连带伤害
等锁的事务占着连接池不放 → 池被排队事务堆满 → 后续请求全部排队 → 接口 5xx。锁冲突从来不只是数据库的事。
# 链路: 锁等待 → DB 池 checked_out 堆满 → 请求排队 → 5xx
# 互证: m115 池使用率 + c17 5xx 错误率
预防死锁的通用心法
两条铁律:① 所有事务按同样顺序拿锁(死结就系不上);② 缩短事务——先算好再开事务,别在事务里做慢查询和外部调用。
# 反面教材: 事务中间去请求 LLM → 锁窗口拉长几百倍
# 正解: 事务里只碰 DB,外部调用挪到事务外

📋 逐面板精讲 2 张图一张不落

🔴 c34 · 数据库锁冲突(近 1h 次数)

它是什么:过去 1 小时数据库“抢座位吵架”了几次。这是锁专项修复(L-21)留下的哨兵:删除节点/换源的重试兜底每触发一次计一次。它不是业务量指标——业务量再大,修干净的锁也不该让它非零。

回答什么问题:锁问题修干净了吗?有没有新的锁窗口冒出来?

✅ 根因修净后恒为 0(<1 绿)——这条曲线最健康的样子就是一条贴地的直线。

🚨 ≥1 黄 / ≥10 红;持续 > 0 = 仍有锁窗口未收敛——立即去 c35 速率图按 operation × code 定位是哪类操作、哪种死法,并对照发布记录找第一出现时间。

# 面板 PromQL(照抄主图谱口径)
sum(increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]))

联动:主图谱 c34 ↗ · 锁冲突率 c35(定位入口) · 相关课程:increase() 与 changes() / HTTP 5xx(连带现场)

🔴 c35 · 数据库锁冲突率(按操作与错误码)

它是什么:吵架的细节——operation(删画布输出 / 节点换源)× code 两条维度拆开的速率线。1205 = 锁等待超时(等太久放弃),1213 = 死锁(俩事务互相等,数据库强制一个滚)。大数字只告诉你吵了几架,这张图告诉你谁跟谁、怎么吵的。

回答什么问题:哪类操作在跟谁抢锁?是超时还是死锁?修复上线后有没有归零并保持?

✅ 全部线贴地;被 L-21 修过的 operation 长期为 0。

🚨 1205 主导 = 有长锁窗口在挡路(查长事务/慢查询/事务内外部调用);1213 主导 = 加锁顺序不一致(查事务的拿锁顺序)——两种死法处置方向不同,别混着修。

# 面板 PromQL(照抄主图谱口径)
sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)

联动:主图谱 c35 ↗ · 哨兵大数字 c34 · 相关课程:DB 连接池(排队连坐) / increase() vs rate()

🏭 生产实战 real world

场景 1 · 早高峰突然一批 500(锁冲突复发)

用户炸群、接口报 500,排查速查表里锁冲突是必查站。三步:哨兵确认 → 定位操作与死法 → 确认连带伤害。

# 1) 哨兵确认:近 1h 几次
sum(increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]))
  # ≥1 黄 / ≥10 红;恒 0 则此路不通,转头查其他依赖

# 2) 定位:哪类操作 × 哪种死法
sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)
  # workspace_output_delete / workspace_node_edge_replace × 1205/1213

# 3) 连带伤害确认:池是不是被排队事务占满(内存盘)
max_over_time(siliang_db_pool_connections{state="checked_out"}[1m])
  # 贴上限 = 锁等待连坐连接池,接口在排队(对照 m115 使用率)

判读:计数归零、池使用率回落、5xx 停止,才算这波过去;事后按 operation 去修对应的锁窗口。

场景 2 · 区分 1205 与 1213 的处置路径

两种死法两种病:1205 找“谁抱着锁不撒手”,1213 找“谁们的拿锁顺序打架”。先按 code 拆开再分别下钻。

# 1205 主导:找“长锁窗口”——谁抱着锁不撒手
sum(rate(siliang_db_lock_errors_total{code="1205"}[5m])) by (operation)
  # 处置:缩短事务,把慢查询/外部调用挪出事务

# 1213 主导:找“加锁顺序不一致”
sum(rate(siliang_db_lock_errors_total{code="1213"}[5m])) by (operation)
  # 处置:统一拿锁顺序(大家先拿同一把再拿下一把),死结就系不上

# 时间维度:第一出现时间对照发布记录
sum(increase(siliang_db_lock_errors_total[1h])) by (operation)
  # 非零起点贴着某次发布 → 新代码引入的新锁窗口

场景 3 · L-21 类修复上线后的观察窗口

修复不是合完代码就结束——上线后 24h 内哨兵必须恒 0,被修的那类 operation 要归零并保持。

# 修复上线后 24h:哨兵应恒 0
sum(increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]))
  # 任何非零 → 回滚评估或继续收敛,别让“先观察几天”拖成复发

# 对照操作维度确认修的就是那一类
sum(increase(siliang_db_lock_errors_total[1h])) by (operation)
  # 被修的 operation 应归零;
  # 别的 operation 冒头 = 新的锁窗口,另开专项

场景 4 · 给锁冲突加“非零即报”的哨兵告警

哨兵的价值在复发第一时间知道。口径与面板一致,非零即报,不要加“再等等看”的缓冲。

# 哨兵告警:非零即报(口径与面板一致)
- alert: SiliangDbLockErrors
  expr: increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]) > 0
  for: 0m
  labels:
    severity: warning
  annotations:
    summary: "近 1h 出现数据库锁冲突(1205/1213)"
    description: "L-21 修复疑似回退或新锁窗口,按 c35 by (operation, code) 定位"

场景 5 · 冲高时刻去 MySQL 侧看谁在持锁

监控告诉你“吵了几架、谁跟谁”,要抓“抱着锁不撒手”的现行,得去数据库里看长事务(通用排查命令)。

# 出事时在 MySQL 侧看谁在持锁(通用排查命令)
SELECT * FROM information_schema.INNODB_TRX ORDER BY trx_started;
  # trx_started 很早还在跑的 = 长事务嫌疑人(锁窗口元凶)

SHOW ENGINE INNODB STATUS;
  # LATEST DETECTED DEADLOCK 段:
  # 最近一次 1213 的双方各持什么锁、各等什么锁
  # 拿到“互等的两张桌子”,就能定加锁顺序该怎么统一

⚠️ 常见坑 pitfalls

坑 1 · 看到 1205 就重启数据库 — 症状:锁超时告警,第一反应重启实例。原因:1205 是应用层等太久主动放弃,重启不缩短任何锁窗口,反而把所有在途事务一起陪葬。正解:找长锁窗口——谁抱着锁不撒手谁就是嫌疑人。
# 错: # 1205 告警 → 重启数据库 → 所有连接断开,事故扩大
# 对: # 查 INNODB_TRX 长事务 + 按 operation 定位锁窗口来源
坑 2 · 1205 和 1213 混为一谈 — 症状:一律“优化 SQL”。原因:1205 是温和超时(重试兜底就行),1213 是死锁回滚(要统一拿锁顺序),病灶不同。正解:先按 code 分流,再分别处置。
# 错: # 只看总量 → 统一“优化 SQL” → 1213 照样复发
# 对: rate(siliang_db_lock_errors_total[5m]) by (code)  # → 按 code 分流
坑 3 · 只看大数字不看维度 — 症状:知道“吵了 15 次”,不知道谁跟谁,修无从下手。原因:c34 是哨兵只报总量,operation × code 才是定位入口。正解:大数字报警、速率图定位,两步走。
# 错: # 只盯 c34 的数字来回刷新
# 对: sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)  # → 去 c35
坑 4 · 把偶发 1 次当事故,或把持续非零当噪音 — 症状:单次非零全员拉群,或反过来持续非零没人管。原因:没对齐哨兵阈值。正解:按口径来:≥1 黄、≥10 红;持续 > 0 = 有锁窗口未收敛,必须收敛到恒 0。
# 错: # “就 1 次而已”/“反正一直有点” → 两个方向都错
# 对: # ≥1 黄 / ≥10 红;目标恒 0,非零即查
坑 5 · 在事务里塞外部调用 — 症状:锁冲突总在高峰期爆发。原因:事务里去调 LLM/HTTP,锁窗口被拉长几百倍,等锁队伍瞬间排爆。正解:先算好再开事务,事务里只碰数据库。
# 错: BEGIN; UPDATE …; call_llm(); UPDATE …; COMMIT;  # → 锁窗口爆炸
# 对: call_llm(); BEGIN; UPDATE …; UPDATE …; COMMIT;  # → 事务只碰 DB
坑 6 · 手工查询忘了 job 过滤 — 症状:自己写的查询数值是面板的两倍。原因:共享端口的 server job 会双计,所有面板都带 {job="siliang_backend_worker"} 排除它。正解:手工查询同样带上 job 过滤。
# 错: sum(increase(siliang_db_lock_errors_total[1h]))                    # → 双计
# 对: sum(increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]))
坑 7 · 以为锁冲突是 DBA 的事 — 症状:把锁工单全转给 DBA。原因:锁窗口的长度由应用代码的事务写法决定——事务多大、拿锁顺序、有没有外部调用,全是应用侧的事。正解:DBA 给现场,应用改写法,两边配合收敛。
# 错: # 锁冲突工单 → 直接转 DBA
# 对: # DBA 抓长事务现场 → 应用侧缩事务/统一拿锁顺序
坑 8 · 用 rate 读“近 1h 次数” — 症状:想报“这小时吵了几架”,写了 rate 得到“每秒 0.003 次”,还得自己换算。原因:累计次数用 increase,速率用 rate,口径别错位。正解:次数 = increase[1h];找趋势才用 rate[5m]。
# 错: sum(rate(siliang_db_lock_errors_total[5m]))   # → 这是速率,不是次数
# 对: sum(increase(siliang_db_lock_errors_total[1h]))  # → 近 1h 总共几次

🎓 费曼自测 合上书,能讲出来才算懂

Q1 · 用“抢座位”向产品经理解释 1205 和 1213 的区别。

参考答案:数据库的每一行数据像图书馆的桌子,改之前要先占座(加锁)。1205 是“等座的人等满规定时间还没等到,主动走了”——温和失败,回头再来(重试);1213 是“A 占了 1 号桌想要 2 号桌,B 占了 2 号桌想要 1 号桌,俩人互相等”——管理员(数据库)看不下去了,直接请走一个(强制回滚),被请走的那个活干了一半白干。所以 1213 更恶劣。

Q2 · c34 大数字是哨兵,它“恒 0”意味着什么?什么数值触发什么动作?

参考答案:恒 0 = L-21 锁专项修复依然有效、没有新的锁窗口冒出来——这是它最健康的样子。≥1 变黄:出现零星锁冲突,去 c35 按 operation × code 定位;≥10 变红:锁窗口在持续挡路,按事故处理(对照发布记录找第一出现时间,抓长事务现行)。哨兵的意义是复发第一时间知道,不是平时好看。

Q3 · 锁冲突为什么会连累那些不碰数据库锁的接口变慢甚至报错?

参考答案:因为等锁的事务占着数据库连接干等——连接池是有限的(backend 是 sync 30 / async 80 / checkpointer 30)。等锁的队伍把池借空后,后续所有请求(哪怕是不碰这些行的简单查询)都借不到连接,只能排队,排队超时就 5xx。所以锁冲突的现场是“一片接口一起变慢”,要在连接池使用率(m115/m116)和 5xx(c17)上互证。

Q4 · L-21 修的是什么?“复发”在监控上长什么样?

参考答案:L-21 是锁专项修复——把“删画布输出 / 节点换源”这两类操作的锁窗口缩短(这是重试兜底曾经反复触发的原因)。复发长什么样:c34 的近 1h 计数从恒 0 变成非零并持续;c35 上被修过的 operation 线重新抬头。处置顺序:先按 code 分流(1205 查长锁窗口、1213 查拿锁顺序),再对照发布记录找引入点。

← 上一课:🟠 计费归因与灵感检索 📚 课程目录 下一课:📊 内存全局水位:先量体温 →