网页打开一串「锟斤拷」,数据库里中文全成了问号,排查到最后十有八九是编码没对上。要把这事说透,得先分清 unicode字符集 和 UTF-8 各自管什么。

字符集管编号,编码管存储

unicode字符集干的事,是给全世界每个字符发一个唯一编号:字母 A 是 U+0041,汉字「中」是 U+4E2D,emoji 也有自己的号段,这张表每年还在扩充。但它不规定编号怎么存成字节,UTF-8、UTF-16 才是存储方案。类比一下:字符集是身份证号,编码是把这个号抄下来用的格式。

为什么一个汉字是 3 个字节

UTF-8 是变长编码:英文字母 1 个字节,和 ASCII 完全兼容;常用汉字一般 3 个字节;生僻字和一部分 emoji 要 4 个字节。另外字段长度也要留意:varchar 之类的字段按字符还是按字节算,不同数据库行为不一样,建表前查清。这也是「程序里数出来的长度和肉眼看到的不一样」的根源——按字节算和按字符算结果不同,一个 3 字节汉字被按字节截断,剩下半截就显示成问号或方块。

三个最常见的乱码现场

  • MySQL 表建成 latin1 字符集,插中文直接报错或存成问号,改表用 CONVERT TO CHARACTER SET utf8mb4,动手前先备份
  • Windows 老软件默认 ANSI 本地编码,和 UTF-8 文件互拷必乱,记事本另存为时选 UTF-8 一般能救回来
  • 字节流用错解码方式,Java 里 new String(bytes, StandardCharsets.UTF_8) 必须和解码端约定一致

一条思路:全链路统一

新项目从第一天就统一 UTF-8:源码文件存成 UTF-8(注意带不带 BOM 的区别),数据库字符集用 utf8mb4,而不是 MySQL 里那个历史遗留的 utf8——它最多 3 字节,emoji 存不进去。HTTP 响应头声明 charset=utf-8,终端和日志也配上。全链路一致,乱码基本绝迹;改造老项目时,先列全所有读写入口逐个确认编码,比出了乱码再到处猜快得多。