一个 VARCHAR(1) 的字段,把整条 DTU 数据链路卡住了。问题不在代码逻辑,而在数据库一个列宽------它只够存一个字符。
一、现象:秤上放了重物,前端数字不动
场景是给加工环节接电子秤:DTU 透传秤的串口数据 → EMQX → 服务端消费解析 → 写回加工任务那行 → 前端轮询显示实时重量。
联调测试的时候,我在秤上放了重物,前端那个重量数字却一动不动。
它不崩、不白屏,也不报错,只是这一路数据默默没了。这种「不报错但数据不对」的问题,比直接抛异常难查得多。
二、排查:三层链路逐层确认
我按链路往下查,一层层排除:
| 层 | 查什么 | 结论 |
|---|---|---|
| MQTT | 帧到底有没有上来 | 通 ------ 帧正常到达 |
| 解析 | 帧里的重量和稳定标志有没有解析出来 | 通 ------ 都解析出来了 |
| 落库 | UPDATE 有没有真的执行成功 | 问题在这一层 |
前两层之所以能直接排除,是因为报错本身就是从 UPDATE 抛出来的------数据既然走到了落库这一步,说明前面两段链路是活的。这也是排查这类问题的省力办法:先看报错发生在哪一层,就说明它前面的都是通的。

三、根因:列宽只够一个字符
最后在数据库这层找到了:
sql
Data too long for column 'stable_status' at row 1
这个列的原始定义是:
sql
ADD COLUMN stable_status VARCHAR(1) DEFAULT '0'
COMMENT '设备稳定状态:0波动中/1已稳定'
VARCHAR(1)------只够存一个字符 ,设计意图就是存 0 或 1。
但落到这个列上的值,并不总是单个字符。
这里有个容易被忽略的设计:电子秤的「稳定标志」其实有两个来源------
- 帧里自带 :DTU 透传上来的报文里可能带稳定标志位。解析器按配置的取值集合判定什么算「已稳定」(默认
1 / true / st / stable / ok / stable-ok都算)。 - 服务端判定:帧里不带标志时,由服务端按「N 秒内重量变化不超过阈值」自行判断(默认窗口 3 秒、容差 0.5kg,容差是留给秤末位抖动的)。
两条路径如果产出的标志格式不统一,就会有一路写出超过一个字符 的值,直接撞上 VARCHAR(1) 的墙。
这个坑的关键不在于「谁写错了」,而在于这个字段允许两套格式并存。

四、为什么难查:三个障眼法
| 障眼法 | 表现 |
|---|---|
| 报错位置和现象位置隔了三层 | 帧 → 解析 → 落库,报错在最后一层,现象在最前面 |
| 异步链路吞异常 | 消费线程抛的异常只进日志,业务侧完全无感 |
| 前端「假装正常」 | 轮询照跑、页面不报错,看不出是断了 |
五、修法:统一口径,不动表结构
把该列的取值统一成数字字典 0 / 1------4 个文件、5 处调用全部对齐,不用改表结构。
(经验:改之前先把这个字段的所有写入点 grep 一遍。只看你眼前那一处,一定会漏。)
六、复盘:怎么防
- 字典类字段,值和列宽一次性定死 ------
VARCHAR(1)这种「刚好够」的设计,一旦有人往里塞别的格式,立刻炸 - 多来源的同一个字段,写库前强制归一化------帧里来的、服务端算的、前端传的,出口只留一种格式
- 异步消费端必须有兜底 + 告警------异常不能只写日志,要能叫醒人
- 给链路加「静默失败」监控------多久没数据就告警,别等用户来投诉
- MySQL 严格模式别关------它报错是在救你;关了它只会静默截断,脏数据悄悄进去更麻烦
附:这套采集里几个真实的取舍
这几个是联调时才想明白的,比技术本身更值钱:
- 写库节流 500ms:秤是高频上报的(一秒好几帧),不做节流会把数据库打满
- 自动兜底默认关闭:现场是「一样料分多趟加」,中途静止会被误判成「加完了」。宁可留空让人补录,也不自动串位
- 租户隔离红线 :采集线程没有登录上下文,必须先按设备反查绑定的是哪个任务、再切到那个任务的租户落库,否则数据会写进
tenant=0(平台层),造成跨租户污染
(本文用「某 SaaS 平台」代指实际项目,表名、字段名、配置项均已替换,不含任何真实客户信息。)