乱码的根因几乎都不是数据本身坏了,而是迁移链路上“导出、传输、导入、连接”四层的字符集不一致:把 utf8mb3(MySQL 里写作 utf8,单字符最多 3 字节)库导进 utf8mb4 库、或者导出时没带 --default-character-set、或应用连接没设 charset,都会让中文按 latin1 解释成 䏿–‡ 这类鬼画符。先判断数据是“显示乱”还是“已经写坏”,再决定用改配置还是用 CONVERT 回滚,前者 5 分钟,后者必须逐列处理。
怎么判断乱码是连接层问题还是数据真的坏了?
结论:用 SELECT HEX(字段) 看真实字节,能立刻区分三种情况,判断错方向会把可救的数据改成不可救。
第一步连上库执行诊断三件套:
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
SHOW CREATE TABLE cms_article\G
再看可疑字段的十六进制:SELECT id, HEX(title) FROM cms_article LIMIT 5;
- 中文“测试”在 utf8mb4 下的 HEX 是
E6B58BE8AF95,说明数据是好的,乱码只出在页面或客户端 → 连接层问题(情况 A)。
- 出现
C3A4C2B8C2ADC3A6E28093 这类成对膨胀的字节,是典型的“utf8 被当 latin1 再存一次”的双重编码 → 可逆修复(情况 B)。
- 字段里全是
3F(问号)或长度明显变短,说明字符在入库时就被替换或截断了 → 不可逆,只能用备份重导(情况 C)。
情况 A:连接层不一致怎么改?
结论:数据没坏时不要动表,只改连接串和配置,改完刷新页面即可恢复。
三处要对齐成同一个值 utf8mb4:数据库服务端(my.cnf 的 [mysqld] 段加 character-set-server=utf8mb4 与 collation-server=utf8mb4_unicode_ci)、客户端命令行([client] 段加 default-character-set=utf8mb4)、应用连接(PHP PDO 用 mysql:host=...;dbname=...;charset=utf8mb4,或连上后立即 SET NAMES utf8mb4)。CMS 侧还要检查自己的配置文件:WordPress 从 4.2 版起支持 utf8mb4,需在 wp-config.php 里把 DB_CHARSET 设为 utf8mb4、DB_COLLATE 留空;Discuz! 等国产 CMS 要把 config/config_global.php 里的 dbcharset 同步改成 utf8mb4。只改数据库不改应用,仍然会乱。
情况 B:数据已被双重编码怎么救?
结论:双重编码是可逆的,用 CONVERT(BINARY CONVERT(col USING latin1) USING utf8mb4) 就能把错解的字节还原,但必须先做全表备份。
先在小范围验证:
SELECT CONVERT(BINARY CONVERT(title USING latin1) USING utf8mb4) AS fixed
FROM cms_article WHERE id = 123;
输出正常中文再批量改,且要连同字段的字符集一起改:
ALTER TABLE cms_article MODIFY title VARCHAR(255)
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
UPDATE cms_article SET title = CONVERT(BINARY CONVERT(title USING latin1) USING utf8mb4);
注意 BINARY 关键字不能省,它是绕过当前字符集校验、强制按原始字节重新解释的关键;省略后会得到 NULL 或继续报错。含 emoji、生僻字的字段必须走 utf8mb4,utf8mb3 存不下 4 字节字符,这是迁移后“部分内容变问号”的直接原因。
迁移时怎么一次性避开字符集坑?
结论:把导出、建库、导入三步的字符集全部显式写死为 utf8mb4,比事后修数据省 10 倍时间。
标准动作:
mysqldump -u root -p --default-character-set=utf8mb4 \
--single-transaction --routines --triggers cmsdb > cmsdb.sql
# 2. 目标库建库时就定死
CREATE DATABASE cmsdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# 3. 导入前在会话里 SET NAMES,再 source
mysql -u root -p --default-character-set=utf8mb4 cmsdb < cmsdb.sql
导入后做两项验证:SELECT COUNT(*) FROM cms_article WHERE title LIKE '%?%' OR title LIKE '%ä%'; 应返回 0;再抽样比对源库与目标库同一行的 MD5(title) 是否一致。
一个高频连带的坑:切到 utf8mb4 后,InnoDB 单列索引长度上限是 3072 字节,varchar(255) 按 4 字节算已达 1020 字节,在 MySQL 5.6 及更早的 COMPACT 行格式下会报 “Specified key was too long”。解决方案是把行格式改为 ROW_FORMAT=DYNAMIC 并确认 innodb_large_prefix=ON(MySQL 5.7.7 起默认开启,8.0 已移除该参数并默认 DYNAMIC),或者把索引列长度收到 varchar(191)。
最后提醒:情况 C 的问号是永久性字符替换,只能从迁移前的备份重导,且重导时务必按上面的 --default-character-set=utf8mb4 参数执行。整套流程的核心就一句话——字符集要在导出、建库、导入、连接四个环节显式统一,任何一环依赖默认值都会在中文上翻车。