锟斤拷三连
UTF-8 字节被误读后二次编码
把一个 GBK 编码的老库整体迁移到 UTF-8 的新服务,是很多团队都会走的一步。表结构、连接串、接口、缓存、日志,五个位置需要同时改,漏掉任何一处都会出问题。
上线当天页面看着正常,第二天开始有用户反馈评论区出现成片的锟斤拷,后台日志里则是整排问号。回滚数据库没有用,因为写入的那一刻字节就已经错了,坏掉的是数据本身,不是显示层。
国产乱码久久久到底卡在链路的哪一环,为什么修了又犯、犯了又修?
九成以上的情况落在三个点上。第一是连接层字符集声明不一致,客户端按 utf8mb4 发送而服务端按 latin1 解析。第二是字节截断,一个三字节的汉字被砍成两字节,解码时得到替换字符。第三是二次转码,UTF-8 字节被当成 GBK 读出来又重新编码回 UTF-8,形成不可逆损坏。顺着这三条线查,比盲改配置快得多。
以下案例来自真实的迁移与排障记录,点击卡片查看现象、成因与处理方式。国产乱码久久久的表现形式虽然相似,但底层成因差别很大,分清类型再动手。
UTF-8 字节被误读后二次编码
字符集不支持导致的替换
字体缺字而非编码错误
不可见前缀破坏解析
按字节截断切坏汉字
多段链路累积误差
界面看到的是渲染结果,字节才是事实。把可疑文本的十六进制打出来,对照编码规则逐字节验证,能避免大量基于猜测的无效修改。国产乱码久久久之所以难缠,多数时候是因为排查方向从一开始就错了。
乱码几乎总是出现在新旧系统交接的位置:老库到新库、旧接口到新网关、本地文件到对象存储。把交界处的字符集声明单独列一张清单,逐项核对,比全局搜索配置更有效率。
把出错的那几个汉字、对应的字节序列、涉及的编码设置单独存成一个最小样例。它既能用来验证修复方案,也能在后续回归时快速确认问题没有复发。
写入、读取、导出、检索、日志、缓存,六个环节都要跑一遍。字符集问题常常修好了主链路却在旁路复发,回归清单是最后一道防线。
连接层字符集声明不一致。客户端按 utf8mb4 发送,服务端按 latin1 解析,字节被原样存下,读出来自然就是问号或方块。先统一链路各环节的字符集声明,再谈数据修复。
问号通常代表字符集在解码阶段就不认识这些字节,信息已经丢失。方块多是字体缺少对应字形,字节本身是完好的,换字体即可恢复。两者处理方向完全不同。
要看损坏发生在哪一步。如果 UTF-8 被误读成 GBK 后没有二次编码,用原始字节重新按 UTF-8 解码还有机会还原。如果已经二次编码写回,属于不可逆丢失,只能靠备份或者人工校对。
三件事。数据库、连接串、应用层统一使用 utf8mb4。所有文本字段在入库前做一次编码合法性校验。日志与接口在传输层明确声明 charset。
多半是环境差异。本地数据库默认字符集与线上不同,或者容器镜像里的 locale 设置不一致。把字符集配置写进代码仓库而不是依赖环境默认值,可以消除这类差异。
先看链路最靠近存储的那一段。数据一旦被错误写入,后面所有环节看到的都是错误结果,从上往下查容易被表象误导。从存储往上逐段验证,定位速度最快。
以下内容为读者分享的排障经历,仅作展示。如果你也遇到过类似情况,欢迎在评论区写下自己的排查过程。
按照文章里的顺序查了一遍,果然是连接串少写了字符集声明。建议再补一份 utf8mb4 的迁移检查清单,方便直接照着走。
我们遇到的国产乱码久久久更隐蔽,出在缓存层的序列化上,读出来的数据看着没问题,写回去就变了样,前后排查了两天才定位到。
半字截断那个案例太真实了,我们的日志切割正好切在汉字中间,每隔一段时间就冒出一段乱码。改成按字符切割之后就再没出现过。