美文网首页数据库
第09期:有关 MySQL 字符集的乱码问题

第09期:有关 MySQL 字符集的乱码问题

作者: 爱可生开源社区 | 来源:发表于2020-07-22 16:35 被阅读0次

    相信大家通过前几篇文章,已经了解了 MySQL 字符集使用相关注意事项。那么数据乱码问题在这儿显得就非常简单了,或许说可能不会出现这样的问题。

    数据之所以会乱码,在 MySQL 里无非有以下几类情况:

    一、转码失败

    在数据写入到表的过程中转码失败,数据库端也没有进行恰当的处理,导致存放在表里的数据乱码。

    针对这种情况,前几篇文章介绍过客户端发送请求到服务端。

    其中任意一个编码不一致,都会导致表里的数据存入不正确的编码而产生乱码。

    比如下面简单一条语句:

    set @a = "文本字符串";
    insert into t1 values(@a);
    

    1.变量 @a 的字符编码是由参数 CHARACTER_SET_CLIENT 决定的,假设此时编码为 A,也就是变量 @a 的编码。
    2.写入语句在发送到 MySQL 服务端之前的编码由 CHARACTER_SET_CONNECTION 决定,假设此时编码为 B。
    3.经过 MySQL 一系列词法,语法解析等处理后,写入到表 t1,表 t1 的编码为 C。

    那这里编码 A、编码 B、编码 C 如果不兼容,写入的数据就直接乱码。

    来看下数据写入过程乱码情况:

    -- 我的终端字符集是 utf8
    
    root@ytt-pc:/home/ytt# locale
    LANG=zh_CN.UTF-8
    LANGUAGE=zh_CN:zh
    LC_CTYPE="zh_CN.UTF-8"
    ...
    LC_IDENTIFICATION="zh_CN.UTF-8"
    LC_ALL=
    
    -- 新建立一个连接,客户端这边字符集为 gb2312
    
    root@ytt-pc:/home/ytt# mysql -S /tmp/mysqld_3305.sock --default-character-set=gb2312
    ...
    mysql> create database ytt_new10;
    Query OK, 1 row affected (0.02 sec)
    
    mysql> use ytt_new10;
    Database changed
    
    -- 表的字符集为 utf8
    mysql> create table t1(a1 varchar(100)) charset utf8mb4;
    Query OK, 0 rows affected (0.04 sec)
    
    -- 插入一条数据,有两条警告信息
    mysql> insert into t1 values ("病毒滚吧!");
    Query OK, 1 row affected, 2 warnings (0.01 sec)
    
    -- 两条警告的内容, 对于字段 a1,内容不正确,但是依然写入了。
    mysql> show warnings\G
    *************************** 1. row ***************************
      Level: Warning
       Code: 1300
    Message: Invalid gb2312 character string: 'E79785'
    *************************** 2. row ***************************
      Level: Warning
       Code: 1366
    Message: Incorrect string value: '\xE7\x97\x85\xE6\xAF\x92...' for column 'a1' at row 1
    2 rows in set (0.00 sec)
    
    -- 那检索出来看到,数据已经不可逆的乱码了。
    mysql> select * from t1;
    +-----------+
    | a1        |
    +-----------+
    | ???▒??▒ |
    +-----------+
    1 row in set (0.00 sec)
    

    那如何防止这种情形出现呢?方法有两种:

    1、把客户端编码设置成和表编码一致或者兼容的编码

    mysql> truncate t1;
    Query OK, 0 rows affected (0.06 sec)
    
    -- 把客户端字符集设置为 utf8mb4   
    mysql> set names utf8mb4;
    Query OK, 0 rows affected (0.00 sec)
    
    -- 数据正常写入
    mysql> insert into t1 values ("病毒滚吧!");
    Query OK, 1 row affected (0.01 sec)
    
    -- 数据正常检索
    mysql> select * from t1;
    +-----------------+
    | a1              |
    +-----------------+
    | 病毒滚吧! |
    +-----------------+
    1 row in set (0.00 sec)
    

    2、设置合适的 SQL_MODE 强制避免不兼容的编码插入数据。

    -- 设置 SQL_MODE 为严格事务表模式
    mysql> set sql_mode = 'STRICT_TRANS_TABLES';
    Query OK, 0 rows affected, 1 warning (0.00 sec)
    
    -- 报错信息由 warnings 变为 error 拒绝插入
    mysql> insert into t1(a1) values ("病毒滚吧!");
    ERROR 1366 (HY000): Incorrect string value: '\xE7\x97\x85\xE6\xAF\x92...' for column 'a1' at row 1
    

    二、客户端乱码

    表数据正常,但是客户端展示后出现乱码。

    这一类场景,指的是从 MySQL 表里拿数据出来返回到客户端,MySQL 里的数据本身没有问题。客户端发送请求到 MySQL,表的编码为 D,从 MySQL 拿到记录结果传输到客户端,此时记录编码为 E(CHARACTER_SET_RESULTS)。

    那以上编码 E 和 D 如果不兼容,检索出来的数据就看起来乱码了。但是由于数据本身没有被破坏,所以换个兼容的编码就可以获取正确的结果。

    这一类又分为以下三个不同的小类:

    1、字段编码和表一致,客户端是不同的编码

    比如下面例子, 表数据的编码是 utf8mb4,而 SESSION 1 发起的连接编码为 gbk。那由于编码不兼容,检索出来的数据肯定为乱码:

    -- SESSION 1
    root@ytt-pc:/home/ytt# mysql -S /tmp/mysqld_3305.sock --default-character-set=gbk;
    ...
    mysql> use ytt_new10;
    Database changed
    
    mysql> show create table t3\G
    *************************** 1. row ***************************
           Table: t3
    Create Table: CREATE TABLE `t3` (
      `a1` varchar(10) DEFAULT NULL,
      `a2` varchar(10) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    1 row in set (0.00 sec)
    
    mysql> select * from t3;
    +--------------+--------------+
    | a1           | a2           |
    +--------------+--------------+
    | ▒▒▒▒▒▒▒▒     | ▒▒▒▒▒▒▒▒     |
    | ▒▒▒▒▒▒▒▒     | ▒▒▒▒▒▒▒˹▒▒▒ |
    | ▒▒▒▒▒▒▒߹▒▒▒ | ▒▒▒▒▒▒▒˹▒▒▒ |
    +--------------+--------------+
    3 rows in set (0.00 sec)
    

    接下来把 SESSION 1 的编码重置为默认 utf8mb4,那查出来的数据一定就是对的。

    mysql> set names default;
    Query OK, 0 rows affected (0.01 sec)
    
    mysql> select * from t3;
    +--------------------+--------------------+
    | a1                 | a2                 |
    +--------------------+--------------------+
    | 病毒快走       | 病毒走了       |
    | 病毒快走       | 病毒走了哈哈 |
    | 病毒快走哈哈 | 病毒走了哈哈 |
    +--------------------+--------------------+
    3 rows in set (0.00 sec)
    

    2、表编码和客户端的编码一致,但是记录之间编码存在不一致的情形

    比如表编码是 utf8mb4,应用端编码也是 utf8mb4,但是表里的数据可能一半编码是 utf8mb4,另外一半是 gbk。那么此时表的数据也是正常的,不过此时采用哪种编码都读不到所有完整的数据。这样数据产生的原因很多,比如其中一种可能性就是表编码多次变更而且每次变更不彻底导致(变更不彻底,我之前的篇章里有介绍)。举个例子,表 t3 的编码之前是 utf8mb4,现在是 gbk,而且两次编码期间都被写入了正常的数据。下面两次 select 查询的结果只有一半是正确的:

    -- 前三条数据编码为 utf8mb4.
    mysql> set names utf8mb4;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from t3;
    +-----------+-----------+
    | a1        | a2        |
    +-----------+-----------+
    | 编码1   | 编码1   |
    | 编码1   | 编码2   |
    | 编码1   | 编码3   |
    | 缂栫爜 | 缂栫爜 |
    | 缂栫爜 | 缂栫爜 |
    | 缂栫爜 | 缂栫爜 |
    +-----------+-----------+
    6 rows in set (0.00 sec)
    
    -- 后三条数据编码为 gbk.
    mysql> set names gbk;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from t3;
    +--------+--------+
    | a1     | a2     |
    +--------+--------+
    | ▒▒▒▒1  | ▒▒▒▒1  |
    | ▒▒▒▒1  | ▒▒▒▒2  |
    | ▒▒▒▒1  | ▒▒▒▒3  |
    | 编码 | 编码 |
    | 编码 | 编码 |
    | 编码 | 编码 |
    +--------+--------+
    6 rows in set (0.01 sec)
    

    那这样的问题该如何解决呢?

    前提是找到两种不同编码记录的分界点!

    比如表 t3 的记录前三条编码和后三条的编码不一致,那可以把两种数据分别导出,再导入到一张改好的表 t4 里。

    -- utf8mb4 的编码数据,前三条导出
    mysql> set names default;select *  from t3 limit 0,3 into outfile '/var/lib/mysql-files/tx.txt';
    Query OK, 0 rows affected (0.00 sec)
    
    Query OK, 3 rows affected (0.00 sec)
    
    -- GBK 编码的数据,后三条导出
    mysql> set names gbk;select *  from t3 limit 3,3 into outfile '/var/lib/mysql-files/ty.txt';
    Query OK, 0 rows affected (0.00 sec)
    
    Query OK, 3 rows affected (0.00 sec)
    
    -- 建立一张新表 t4,编码改为统一的 utf8mb4
    mysql> create table t4 (a1 varchar(10),a2 varchar(10)) charset utf8mb4;
    Query OK, 0 rows affected (0.04 sec)
    
    -- 分别导入两部分数据
    mysql> load data infile '/var/lib/mysql-files/tx.txt' into table t4 character set gbk;
    Query OK, 3 rows affected (0.01 sec)
    Records: 3  Deleted: 0  Skipped: 0  Warnings: 0
    
    mysql> load data infile '/var/lib/mysql-files/ty.txt' into table t4 ;
    Query OK, 3 rows affected (0.01 sec)
    Records: 3  Deleted: 0  Skipped: 0  Warnings: 0
    
    -- 接下来看结果,一切正常
    mysql> set names default;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from t4;
    +---------+---------+
    | a1      | a2      |
    +---------+---------+
    | 编码  | 编码  |
    | 编码  | 编码  |
    | 编码  | 编码  |
    | 编码1 | 编码1 |
    | 编码1 | 编码2 |
    | 编码1 | 编码3 |
    +---------+---------+
    6 rows in set (0.00 sec)
    
    -- 完了把原来的表删掉,新表 t4 改名即可。
    mysql> drop table t3;
    Query OK, 0 rows affected (0.04 sec)
    
    mysql> alter table t4 rename to t3;
    Query OK, 0 rows affected (0.04 sec)
    
    -- 再次查看记录,一切正常
    mysql> select * from t3;
    +---------+---------+
    | a1      | a2      |
    +---------+---------+
    | 编码1 | 编码1 |
    | 编码1 | 编码2 |
    | 编码1 | 编码3 |
    | 编码  | 编码  |
    | 编码  | 编码  |
    | 编码  | 编码  |
    +---------+---------+
    6 rows in set (0.00 sec)
    

    3、每个字段的编码不一致,导致乱码

    和第二点一样的场景。不同的是:非记录间的编码不统一,而是每个字段编码不统一。举个例子,表 c1 字段 a1,a2。a1 编码 gbk,a2 编码是 utf8mb4。那每个字段单独读出来数据是完整的,但是所有字段一起读出来,数据总会有一部分乱码。具体看下面的示例:

    -- 字段 a1 编码 GBK,读出来正常,字段 a2 不正常。
    mysql >set names gbk;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql >select * from c1;
    +--------------+----------------+
    | a1           | a2             |
    +--------------+----------------+
    | 我在中国     | ▒▒▒▒▒й▒▒▒ã▒             |
    | 你在日本     | ▒▒▒▒▒й▒▒▒ã▒             |
    | 你在韩国     | ▒▒▒▒▒й▒▒▒ã▒             |
    | 你在美国     | ▒▒▒▒▒й▒▒▒ã▒             |
    | 中国太好     | ▒▒▒▒▒й▒▒▒ã▒             |
    | 中国太棒     | ▒▒▒▒▒й▒▒▒ã▒             |
    +--------------+----------------+
    6 rows in set (0.00 sec)
    
    -- 以编码 utf8mb4 来获取字段 a1 的值,显示不正常,字段 a2 读出来正常。
    mysql >set names utf8mb4;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql >select * from c1;
    +--------------------+-----------------------+
    | a1                 | a2                    |
    +--------------------+-----------------------+
    | 鎴戝湪涓?浗        | 还是中国最好!        |
    | 浣犲湪鏃ユ湰       | 还是中国最好!        |
    | 浣犲湪闊╁浗        | 还是中国最好!        |
    | 浣犲湪缇庡浗       | 还是中国最好!        |
    | 涓?浗澶?ソ         | 还是中国最好!        |
    | 涓?浗澶??          | 还是中国最好!        |
    +--------------------+-----------------------+
    6 rows in set (0.00 sec)
    

    以上结果怎么能一种编码的方式正常显示呢?也是类似第二种解决方式,把数据导出来,再导进去。由于 MySQL 处理数据是按照行的方式,按照列的方式会麻烦一点,我这里用 OS 层来合并导出的文件,再导入到 MySQL 表里。

    -- 分别按列导出两个文件
    mysql >select a2 from c1 into outfile '/var/lib/mysql-files/c1_a2.txt';
    Query OK, 6 rows affected (0.01 sec)
    
    mysql >select a1 from c1 into outfile '/var/lib/mysql-files/c1_a1.txt';
    Query OK, 6 rows affected (0.00 sec)
    
    -- OS 层用paste命令合并这两个文件
    [root@ytt-pc mysql-files]# paste c1_a1.txt c1_a2.txt  > c1.txt
    
    -- 创建表c2,编码统一。
    mysql >create table c2 (a1 varchar(10),a2 varchar(10)) charset utf8mb4;
    Query OK, 0 rows affected (0.02 sec)
    
    -- 导入合成后的文件到表c2
    mysql >load data infile '/var/lib/mysql-files/c1.txt' into table c2 ;
    Query OK, 6 rows affected (0.00 sec)
    Records: 6  Deleted: 0  Skipped: 0  Warnings: 0
    
    -- 删除表c1,重命名表c2为c1。
    mysql >drop table c1;
    Query OK, 0 rows affected (0.02 sec)
    
    mysql >alter table c2 rename to c1;
    Query OK, 0 rows affected (0.02 sec)
    
    -- 显示结果正常,问题得到解决。
    mysql >select * from c1;
    +--------------+-----------------------+
    | a1           | a2                    |
    +--------------+-----------------------+
    | 我在中国     | 还是中国最好!        |
    | 你在日本     | 还是中国最好!        |
    | 你在韩国     | 还是中国最好!        |
    | 你在美国     | 还是中国最好!        |
    | 中国太好     | 还是中国最好!        |
    | 中国太棒     | 还是中国最好!        |
    +--------------+-----------------------+
    6 rows in set (0.00 sec)
    

    三、LATIN1

    还有一种情形就是以 LATIN1 的编码存储数据

    估计大家都知道字符集 LATIN1,LATIN1 对所有字符都是单字节流处理,遇到不能处理的字节流,保持原样,那么在以上两种存入和检索的过程中都能保证数据一致,所以 MySQL 长期以来默认的编码都是 LATIN1。这种情形,看起来也没啥不对的点,数据也没乱码,那为什么还有选用其他的编码呢?原因就是对字符存储的字节数不一样,比如 emoji 字符 "❤",如果用 utf8mb4 存储,占用 3 个字节,那 varchar(12) 就能存放 12 个字符,但是换成 LATIN1,只能存 4 个字符。来看下这个例子就明白了。

    -- 更改数据库 ytt_new10 字符集为 LATIN1
    mysql> alter database ytt_new10 charset latin1;
    Query OK, 1 row affected (0.02 sec)
    
    mysql> set names latin1;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> use ytt_new10;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    
    -- 创建表 t2,默认字符集为 LATIN1
    mysql> create table t2(a1 varchar(12));
    Query OK, 0 rows affected (0.05 sec)
    
    -- 插入emoji字符,只能插入4个字符
    mysql> insert into t2 values ('❤❤❤❤');
    Query OK, 1 row affected (0.02 sec)
    
    -- 检索出来结果完全正确
    mysql> select * from t2;
    +--------------+
    | a1           |
    +--------------+
    | ❤❤❤❤         |
    +--------------+
    1 row in set (0.00 sec)
    
    -- 但是在加一个字符,插入第五个字符报错。
    mysql> insert into t2 values ('❤❤❤❤❤');
    ERROR 1406 (22001): Data too long for column 'a1' at row 1
    
    -- 换张表t3,字符集为utf8mb4.
    mysql> create table t3 (a1 varchar(12)) charset utf8mb4;
    Query OK, 0 rows affected (0.06 sec)
    
    -- 结果集的字符集也设置为utf8mb4.
    mysql> set names utf8mb4;
    Query OK, 0 rows affected (0.00 sec)
    
    -- 插入12个'❤',也就是同样的表结构,存储的字符串比latin1多。
    mysql> insert into t3 values (rpad('❤',12,'❤'));
    Query OK, 1 row affected (0.01 sec)
    
    mysql> select * from t3;
    +--------------------------------------+
    | a1                                   |
    +--------------------------------------+
    | ❤❤❤❤❤❤❤❤❤❤❤❤                         |
    +--------------------------------------+
    1 row in set (0.00 sec)
    

    其实 MySQL 一直到发布了 8.0 才把默认字符集改为 utf8mb4。比如现在依然是表 t2,如果想把编码改为 utf8mb4。那之前的数据必然没法正常显式:

    -- 改为 utf8mb4
    mysql> set names utf8mb4;
    Query OK, 0 rows affected (0.00 sec)
    
    -- 数据显式乱码
    mysql> select * from t2;
    +--------------------------+
    | a1                       |
    +--------------------------+
    | ����             |
    +--------------------------+
    1 row in set (0.00 sec)
    

    怎么解决这个问题。有两种方法:

    1、把表 t2 的列 a1 先改为二进制类型,在改回来用 utf8mb4 的编码的字符类型。

    -- 现改为 binary 类型
    mysql> alter table t2 modify a1 binary(12);
    Query OK, 1 row affected (0.11 sec)
    Records: 1  Duplicates: 0  Warnings: 0
    
    mysql> select * from t2;
    +----------------------------+
    | a1                         |
    +----------------------------+
    | 0xE29DA4E29DA4E29DA4E29DA4 |
    +----------------------------+
    1 row in set (0.00 sec)
    
    -- 再改为varchar(12) utf8mb4.
    mysql> alter table t2 modify a1 varchar(12) charset utf8mb4;
    Query OK, 1 row affected (0.15 sec)
    Records: 1  Duplicates: 0  Warnings: 0
    
    -- 数据就正常显式。
    mysql> select * from t2;
    +--------------+
    | a1           |
    +--------------+
    | ❤❤❤❤         |
    +--------------+
    1 row in set (0.00 sec)
    
    -- 接下来,再把表的字符集改回UTF8MB4。
    mysql> alter table t2 charset utf8mb4;
    Query OK, 0 rows affected (0.02 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    

    2、还是用最土的方法,把数据导出来,把表编码修改好,再把数据导入到表里。

    -- 导出表t2数据。
    mysql> select * from t2 into outfile '/var/lib/mysql-files/t2.dat';
    Query OK, 1 row affected (0.00 sec)
    
    -- 删除表
    mysql> drop table t2;
    Query OK, 0 rows affected (0.07 sec)
    
    -- 重建表,编码为utf8mb4.
    mysql> create table t2(a1 varchar(12)) charset utf8mb4;
    Query OK, 0 rows affected (0.05 sec)
    
    mysql> set names utf8mb4;
    Query OK, 0 rows affected (0.00 sec)
    
    -- 导入之前导出来的数据
    mysql> load data infile '/var/lib/mysql-files/t2.dat' into table t2;
    Query OK, 1 row affected (0.01 sec)
    Records: 1  Deleted: 0  Skipped: 0  Warnings: 0
    
    -- 检索完全正常。
    mysql> select * from t2;
    +--------------+
    | a1           |
    +--------------+
    | ❤❤❤❤         |
    +--------------+
    1 row in set (0.00 sec)
    

    总结

    通过上面的详细说明,相信对 MySQL 乱码问题已经有一个很好的了解了。那来回顾下本篇的内容。本篇主要列列举了 MySQL 乱码可能出现的场景,并对应给出详细的处理方法以及相关建议,希望以后大家永远不会出现乱码问题。


    关于 MySQL 的技术内容,你们还有什么想知道的吗?赶紧留言告诉小编吧!

    相关文章

      网友评论

        本文标题:第09期:有关 MySQL 字符集的乱码问题

        本文链接:https://www.haomeiwen.com/subject/caciihtx.html