前言
“数据库的数据变成乱码了!”---想必不少 DBA 们对类似的“呼救”不算太陌生。一般来说这类问题都是字符集的设置有关,同时在 MySQL 中也存在“错入错出”的这种“神话”:登录到数据库看的时候是乱码,代码/WEB 上显示的是正常的。
这里通过构造一个测试示例,来理一理字符集和“错入错出”这两个话题。
字符集与字符编码
想理清乱码的原因,需要先对字符集与字符编码有一个简单的了解:
- 字符集:各种文字和符号的集合,不同集合支持的字符范围存在差异,有的字符集支持中英文,有的只支持英文。
- 字符编码:文字和符号集合与数字系统之间建立对应关系。
简单来看的话,MySQL 或者 WEB 应用上推荐使用的 utf8mb4 指的就是字符编码,对应的是字符集是 Unicode,utf8mb4 的编码决定了 Unicode 字符集中的文字和符号要如何转化成二进制数据流来进行传输。
抛开同一个字符集之内不同字符编码的兼容性,如果一串通过 utf8mb4 编码的“正常的二进制数据流”用 UTF-16 编码方式来解析的话,显示出来的就会是“乱码”。
如果是跨字符集的不同编码方式,比如 GBK 和 utf8mb4 之间,不通过字符集的转换而直接进行编码和解码,必然会得到一串“乱码”。
出现“乱码”的原因是各个字符编码的规则中,如果遇到自己无法识别的编码,会直接把无法识别的编码替换成一个特殊的编码来代表这个字符无法识别,因此在转码的过程中,真实的编码已经丢失了。
在 MySQL 的环境下
MySQL 涉及到字符集的参数比较多,详细的作用建议参考官方文档。与 Client 交互的时候,character_set_client
是其主要作用的参数。
一行数据从 Client 端发出,到存储在 MySQL 中,再被 Client 读取到,可以参考如下的数据流转简图:
可以发现不论是把数据存进 MySQL 还是从 MySQL 读出来数据,都会经过 3 次字符的解码或者编码。结合上面对于字符集的简介,很容易就能构造出典型的乱码场景:
- 程序端使用 GBK 字符编码写入中文
- MySQL 根据 character_set_client 的设置:utfutfmb4 字符编码来解码 Client 端的内容。
- MySQL 根据表的字符编码:utf8mb4 进行转码,写入数据文件
- 此时写入数据文件的内容是正常的?。
- MySQL 根据表的字符编码:utf8mb4 从数据文件解码数据
- MySQL 根据 character_set_client 的设置:utf8mb4 字符编码来编码,把数据传给 Client 端
- Client 端使用 utf8mb4 字符编码解码收到的数据
- 此时读出来的数据是乱码
那么实际来测试一下这个场景,看看实际的效果~为了方便起见,这里用 shell 下的 MySQL Client 来模拟程序的 Client 端,并更改character_set_results
来模拟终端/WEB的展示效果。
创建一个测试用的表:
代码语言:txt复制CREATE TABLE `char_utf8mb4` (
`name` varchar(16) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
场景1:
先准备好脚本:
然后写入数据看看效果:
可以看到 warnings 中会有报错信息,且实际显示出来的内容也是乱码。
那么到底是写入的数据就已经是乱码了,还是说内容没问题,只是显示异常?更改一下字符集编码试试:
发现还是乱码,说明写入的数据已经是乱码了,无法得知原来的数据是什么。
错入错出的 MySQL
关于字符集的问题,其实在 MySQL 之中还有一种“错入错出”的现象:即用错误的字符编码写入了数据,但是用同样错误的字符编码读取的时候发现内容还是正常的。
参考之前转码的简介可以知道,如果想要达到这种效果,需要字符多次转码的过程中不能丢失真实的编码。
而 MySQL 以前默认的字符集 Latin1 有一个特性:在遇到自己无法表示的字符时,会保留原字符集的编码数据,并跳过忽略该字符进而处理后面的数据。
那么这次替换一下表的字符编码,再试一试:
可以看到使用 gbk 编码的时候,可以正常看到中文,但是切换到 utf8mb4 编码的时候,就变成乱码了,说明使用 Latin1 编码写入数据的时候,并没有因为转码而丢失真实的编码。因此达到了错入错出的效果:用“错误的”编码方式向 Latin1 的表写入了数据,用其他的编码方式读出来是乱码,但是继续用“错误”的编码方式可以读出正确的内容。
总结一下
其实大多数时候,只要让 MySQL,表,客户端的字符集完全保持一致就不会猜到这些坑,但是当问题已经发生了之后,了解过相关的知识会更快速的进行定位故障,评估恢复数据的可能性。