崩溃复查与"回声"bug:异常码判案、漏网之鱼和耳塞开关
2026-08-21
昨天修了"数据库瞬断打死程序"的 bug,今天现场又崩了三次。复盘后发现两个教训:"修崩溃要搜干净所有隐患点"和"改完代码要确认部署"。另外今天还揪出一个"点几下才发作"的界面 bug------三个环节互相触发转圈。一篇讲完。
一、又崩了:排除法四查,锁定"没接住的异常"
今天现场程序又崩了 3 次,报错和昨天一模一样。这次用了排除法逐项排查:
| 查什么 | 怎么查 | 结果 |
|---|---|---|
| 数据库崩了没 | 看 binlog 时间戳(还在写=没崩)+ 数据库错误日志 | 没崩 ✅ |
| 连接数耗尽没 | 查最大连接数配置 + 当前连接数 | 没满(2/151)✅ |
| 系统网络/磁盘故障没 | 查系统事件日志 | 无故障 ✅ |
| 程序自己扛住没 | 看崩溃异常码 | 没扛住 ❌ |
前三项全排除,凶手还在程序自己身上。
生活类比:人死了,先查死因------是他杀(数据库挂了)、饿死(连接耗尽)、意外(系统故障),还是自身疾病(程序没接住异常)。四项逐一排除,最后一项就是答案。
二、概念:异常码是"死亡报告单"
崩溃时系统会留下一个异常码,它就是一纸死亡报告:
| 异常码 | 含义 | 结论 |
|---|---|---|
0xe0434352 |
托管异常没被捕获(程序抛了没接住的异常) | 程序自身的 bug,可修 |
0xc0000005 |
内存访问越界 | 野指针/越界类问题 |
0xc00000fd |
栈溢出 | 无限递归类问题 |
看到 0xe0434352 = 好消息:它明确告诉你"有代码 throw 了、没人 catch",而不是玄学硬件问题。找到没接住的位置,包上 try-catch 就修好了。
类比:医生看到化验单上的特定指标,就能判断是病毒(可治)还是基因病(难治)。0xe0434352 就是那个"可治"的指标。
三、概念:为什么认证插件"多一步"更容易被瞬断打断
报错里总有认证插件的身影(caching_sha2_password)。它不是根因,但它解释了为什么瞬断总发生在认证这一步:
- 新认证插件(caching_sha2_password)比老插件(mysql_native_password)多一次 RSA 公钥交换------连接建立过程更长、步骤更多
- 瞬断是随机的,步骤越多,被打断的概率越大------所以断连总是"恰好"死在认证环节,留下误导性的报错
- 换成老插件或允许客户端取公钥,能减少 断连概率,但根治不了------连接永远可能偶发失败
核心认知:数据库没坏 ≠ 不用修程序。网络的世界没有"永不失败",程序必须自己 try-catch + 重试。
四、教训一:修崩溃要"搜干净",别漏网
昨天修了 2 个崩溃点,今天又崩------因为还有第 3 个:保存生产记录的后台方法里也查了数据库,同样没包 try-catch。
教训 :修崩溃时,先搜索所有数据库操作点,逐个检查有没有异常保护。修一个漏一个,等于没修。
排查句式:找出"所有会查库/连库的地方",列清单,逐个过------清单式排查,而不是想起哪个修哪个。
类比:家里闹老鼠,补了一个洞就以为太平了。正确的做法是全屋搜洞,补完一个必须问"还有没有下一个洞"。
五、教训二:改完代码 ≠ 修好了,还要部署
更扎心的一层:现场机器跑的还是旧版程序 ------昨天的修复根本没部署上去。改完代码要确认部署,否则修了也白修。
类比:医生开好了药方,但病人吃的还是昨天的旧药。药方再好,没吃进嘴等于零。
六、新 bug:三个环节互相触发的"回声"
今天另一个 bug 很有意思------界面列表连点几下才开始发作:列表自动滚、条目自动选中、图形自动高亮,来回跳停不下来。
根因:事件回环自激(三环相扣)
列表选中 ──绑定──> 图形聚焦状态 ──变化──> 监听器发现"变了"
↑ │
└────── 滚动方法又写了一次"选中" <─────────┘
- 列表条目选中 ↔ 图形的聚焦状态,两者双向绑定
- 监听器盯住聚焦状态,一变就"改选中 + 滚列表"
- 滚动方法里又写了一句"选中 = 这个图形"------等于又改了一次选中
第 3 步还是异步排队的:连点几下,队列堆满,A→B→C 来回跳,停不下来。
特征识别 :这类 bug 不是一点就崩,而是"点几下才发作 ""来回跳 "------看到这种描述,第一反应就应该是反馈回环。
排查小技巧 :先定位"监听器"(谁订阅了变化事件)→ 再找"回写点"(谁在回调里又设置了一次选中/聚焦)→ 中间那条"又改一次"的语句,往往就是元凶。
七、语法:抑制标志 + try/finally(耳塞开关)
修复思路:程序自己滚列表时,让监听器"装聋"。
csharp
// ① 加一个开关(默认关)
private bool 抑制标志 = false;
// ② 监听器开头检查开关:开着就 return,不响应
void 监听器(参数)
{
if (抑制标志) return; // 耳塞戴着,听不见
...原来的滚动逻辑...
}
// ③ 程序自己回写选中时:戴耳塞 → 写 → 摘耳塞
void 滚动定位方法(目标)
{
抑制标志 = true; // 戴耳塞
try
{
选中 = 目标; // 音箱出声(触发监听器,但被耳塞挡住)
}
finally
{
抑制标志 = false; // 摘耳塞------finally 保证一定会摘
}
}
耳塞比喻:麦克风(监听器)对着音箱(滚动方法)。
- 音箱播放前,给麦克风戴耳塞(标志=true)
- 音箱自己的声音(回写触发的聚焦变化),麦克风听不见(return)
- 播完摘耳塞(标志=false)
- 用户自己点(不戴耳塞)→ 麦克风正常听见 → 照常响应
语法详讲 try/finally :finally 里的代码无论 try 里正常结束还是抛异常,都一定会执行------用它摘耳塞,保证麦克风不会永远聋着(标志不会卡死在 true,否则用户手点也失效了)。
通用教训 :任何"监听 X 变化 → 又去改 X"的写法都要警惕------改之前先问自己一句:"这次改动会不会再触发我自己?"。异步回调里的回写更隐蔽(把一次振荡放大成排队级联),比同步死循环难查。
八、试卷通用概念重点讲
今天批改的试卷里有几个通用概念值得展开------它们不局限于某个项目,是行业通识。
1. 判定字段的取值语义(1=OK、0=NG 的坑)
判定结果字段,值约定是 1 = OK,0 = NG 。坑在哪?字段名以"NG"字样开头,极易误读成"0 才是合格"------实际是 1 = OK。
通用教训 :字段名的字面意思 ≠ 取值语义。读代码要看赋值处 (判定值 == 1 ? "合格" : "不合格"),别靠名字猜。
类比:门牌写着"非营业时间",实际含义却是"1=开门"。门牌是给人看的,含义要看实际开没开。
2. 生产者-消费者通道(Channel)
场景:探测器(生产者)不断拍图,处理程序(消费者)逐张处理------两边速度不匹配,中间需要一个缓冲。
生产者(拍图) ──WriteAsync──> [ 传送带 Channel ] ──TryRead──> 消费者(处理)
(无限长,先进先出)
Channel.CreateUnbounded<T>()= 造一条现成的传送带Writer.WriteAsync(x)= 把货放上去Reader.TryRead(out x)= 把货取下来
对比 :ConcurrentQueue + ManualResetEvent 是"手工做传送带"(线程安全队列 + 信号灯自己摆);Channel 是"成品传送带"------有现成的,别自己造。
3. 背钻与灰度阈值(图像里怎么"量"深度)
概念 :PCB 通孔镀铜后,孔里会残留多余铜段(stub),高频信号经过会反射------背钻就是把这截多余铜钻掉。
检测逻辑:量钻孔深度对不对------太浅残留 stub、太深伤到板层,都判不合格。
图像原理 :切片图里铜暗、孔亮;灰度阈值就是亮度分界线,靠它找孔壁边界、量出深度。
表面 ▓▓▓▓▓▓▓▓▓▓
镀铜 ▓▓░░░░░░▓▓ ← 孔里多余的铜(stub)要钻掉
板层 ▓▓▓▓▓▓▓▓▓▓
背钻深度:太浅留 stub / 太深伤层
坑:灰度阈值、信号系数、深度范围是三个不同参数,别搞混。
4. SECS/GEM 三件套(半导体设备通信标准)
设备要和工厂 MES 系统对话,靠的是行业标准三件套:
| 组件 | 管什么 |
|---|---|
| SECS-II | 报文格式(信怎么写) |
| GEM | 设备模型(设备该报告什么、听什么命令) |
| HSMS | TCP 传输(信怎么送) |
记法 :.NET 实现库的名字 = "SECS for .NET" 的缩写------凡是跟 SECS 相关的库,名字里必有 "SECS" 字样,选项里出现别的名字就是干扰项。
类比:三件套 = 信封格式 + 岗位职责 + 快递公司。
5. 切格子 vs 走路径(两个算法类型,最爱混着考)
| 类型 | 管什么 | 取值含义 |
|---|---|---|
| 切格子(Generate) | 视野怎么切 | 0 = 全局最优(可跨板);1 = 一个视野只含一块板的元件 |
| 走路径(Path) | 拍摄顺序怎么排 | 0 = 整体规划;1 = 一块板一个规划 |
记法:Gen = Generate(切格子)、Path = 路径(走顺序)。题目问的是哪个,答案就是哪个------把"走路径"的描述安到"切格子"头上,就是错因。
类比:切格子 = 蛋糕分几块;走路径 = 先吃哪块的顺序。
6. 权限控制的两种显示状态
机制 :要什么权限 + 无权限时怎么显示。注意------只有两种显示状态:
| 状态 | 效果 |
|---|---|
| 隐藏(Collapsed) | 控件完全消失,位置都不占 |
| 禁用(Disabled) | 看得见,点不了 |
坑 :题目会塞"第三种状态"当干扰项,或者塞"允许/拒绝"当选项------那是权限结论 ,不是显示状态,两个维度别混淆。
7. 占比类阈值的判定模式
还有一类通用判定:计算某成分占比,和阈值比较 (比如填充率类指标)------高于阈值合格、低于不合格(或反之)。记住这是"占比 vs 阈值"的判定模式即可,具体阈值是配方参数,换机种就变,不背数值。
附录:今日概念速查
试卷通用概念(判定字段语义 / Channel / 背钻阈值 / SECS/GEM / 切格子与走路径 / 权限显示状态)详见第八章。
| 概念 | 一句话 |
|---|---|
0xe0434352 |
托管异常没被捕获------"程序有 bug,可修" |
0xc0000005 / 0xc00000fd |
内存越界 / 栈溢出 |
| caching_sha2 认证 | 多一次公钥交换,瞬断更容易打断它------不是根因 |
| 排除法查崩溃 | 库没崩 + 连接没满 + 系统没事 = 程序自己的锅 |
| 事件回环自激 | 监听 X 又改 X,三环相扣转圈;特征"点几下才发作" |
| 抑制标志 + try/finally | 耳塞开关;finally 保证摘耳塞 |
| 合并撞号检测 | `grep -ohE "编号-0-9+" |