标签: 公众号文章mysql
咱们有必要说明一下,字符
实际上是面向人类的一个概念,计算机可并不关心字符是什么,它只关心这个字符对应的字节编码是什么。对于一个字节序列,计算机怎么知道它是使用什么字符集编码的呢?计算机不知道,因此其实在计算机中表示一个字符串时,都须要附带上它对应的字符集是什么,就像这样(以C++语言为例):sql
class String {
byte* content;
CHARSET_INFO* charset;
}
复制代码
比方说咱们如今有一个以utf8
字符集编码的汉字'我'
,那么意味着计算机中不只仅要存储'我'
的utf8编码0xE68891
,还须要存储它是使用什么字符集编码的信息,就像这样:数组
{
content: 0xE68891;
charset: utf8;
}
复制代码
计算机内部包含将一种字符集转换成另外一种字符集的函数库,也就是某个字符在某种字符集下的编码能够很顺利的转换为另外一种字符集的编码,咱们将这个过程称之为字符集转换
。比方说咱们能够将上述采用utf8字符集编码的字符'我',转换成gbk字符集编码的形式,就变成了这样:bash
{
content: 0xCED2;
charset: gbk;
}
复制代码
小贴士: 咱们上边所说的'编码'能够看成动词,也能够看成名词来理解。看成动词的话意味着将一个字符映射到一个字节序列的过程,看成名词的话意味着一个字符对应的字节序列。你们根据上下文理解'编码'的含义。 服务器
MySQL客户端发送给服务器的请求以及服务器发送给客户端的响应其实都是听从必定格式的,咱们把它们通讯过程当中事先规定好的数据格式称之为MySQL通讯协议,这个协议是公开的,咱们能够简单的使用wireshark等截包软件十分方便的分析这个通讯协议。在了解了这个通讯协议以后,咱们甚至能够动手制做本身的客户端软件。市面上的MySQL客户端软件多种多样,咱们并不想各个都分析一下,如今只选取在MySQL安装目录的bin
目录下自带的mysql
程序(此处的mysql
程序指的是名字叫作mysql
的一个可执行文件),如图所示: 函数
小贴士: 咱们这里的'黑框框'指的是Windows操做系统中的cmd.exe或者UNIX系统中的Shell。 学习
咱们一般是按照下述步骤使用MySQL的:ui
下边咱们就详细分析一下每一个步骤中都影响到了哪些字符集。编码
每一个MySQL客户端都维护者一个客户端默认字符集,这个默认字符集按照下边的套路进行取值:spa
自动检测操做系统使用的字符集
MySQL客户端会在启动时检测操做系统当前使用的字符集,并按照必定规则映射成为MySQL支持的一些字符集(一般是操做系统当前使用什么字符集,就映射为何字符集,有一些特殊状况,比方说若是操做系统当前使用的是ascii字符集,会被映射为latin1字符集)。
当咱们使用UNIX操做系统时
此时会调用操做系统提供的nl_langinfo(CODESET)
函数来获取操做系统当前正在使用的字符集,而这个函数的结果是依赖LC_ALL
、LC_CTYPE
、LANG
这三个环境变量的。其中LC_ALL
的优先级比LC_CTYPE
高,LC_CTYPE
的优先级比LANG
高。也就是说若是设置了LC_ALL
,不论有没有设置LC_CTYPE
或者LANG
,最终都以LC_ALL
为准;若是没有设置LC_ALL
,那么就以LC_CTYPE
为准;若是既没有设置LC_ALL
也没有设置LC_CTYPE
,就以LANG
为准。比方说咱们将环境变量LC_ALL
设置为zh_CN.UTF-8
,就像这样:
export LC_ALL=zh_CN.UTF-8
复制代码
那么咱们在黑框框里启动MySQL客户端时,MySQL客户端就会检测到这个操做系统使用的是utf8
字符集,并将客户端默认字符集设置为utf8
。
固然,若是这三个环境变量都没有设置,那么nl_langinfo(CODESET)
函数将返回操做系统默认的字符集,比方说在个人macOS 10.15.3
操做系统中,该默认字符集为:
US-ASCII
复制代码
此时MySQL客户端的默认字符集将会被设置为latin1
。
另外,咱们这里还须要强调一下,咱们使用的黑框框展现字符的时候有一个本身特有的字符集,好比在个人mac上使用iTerm2
做为黑框框,咱们能够打开:Preferences->Profiles->Terminal选项卡,能够看到iTerm2
使用utf8
来展现字符:
LC_ALL
属性设置成GBK
,那么咱们再向黑框框上输入汉字的话,屏幕都不会显示了,就像这样(以下图所示,我敲击了汉字'我'
的效果):
当咱们使用Windows操做系统时
此时会调用操做系统提供的GetConsoleCP
函数来获取操做系统当前正在使用的字符集。在Windows里,会把当前cmd.exe使用的字符集映射到一个数字,称之为代码页(英文名:code page
),咱们能够经过右键点击cmd.exe
标题栏,而后点击属性->选项,以下图所示,当前代码页
的值是936,表明当前cmd.exe使用gbk字符集:
chcp
命令直接看到当前code page是什么:
gbk
字符集,并将客户端默认字符集设置为gbk
。咱们前边提到的utf8字符集对应的代码页为65001
,若是当前代码页的值为65001,以后再启动MySQL客户端,那么客户端的默认字符集就会变成utf8
。 若是MySQL不支持自动检测到的操做系统当前正在使用的字符集,或者在某些状况下不容许自动检测的话,MySQL会使用它本身的内建的默认字符集做为客户端默认字符集。这个内建的默认字符集在MySQL 5.7
以及以前的版本中是latin1
,在MySQL 8.0
中修改成了utf8mb4
。
使用了default-character-set
启动参数
若是咱们在启动MySQL客户端是使用了default-character-set
启动参数,那么客户端的默认字符集将再也不检测操做系统当前正在使用的字符集,而是直接使用启动参数default-character-set
所指定的值。比方说咱们使用以下命令来启动客户端:
mysql --default-character-set=utf8
复制代码
那么不论咱们使用什么操做系统,操做系统目前使用的字符集是什么,咱们都将会以utf8做为MySQL客户端的默认字符集。
在确认了MySQL客户端默认字符集以后,客户端就会向服务器发起登录请求,传输一些诸如用户名、密码等信息,在这个请求里就会包含客户端使用的默认字符集是什么的信息,服务器收到后就明白了稍后客户端即将发送过来的请求是采用什么字符集编码的,本身生成的响应应该以什么字符集编码了(剧透一下:其实服务器在明白了客户端使用的默认字符集以后,就会将character_set_client
、character_set_connection
以及character_set_result
这几个系统变量均设置为该值)。
登录成功以后,咱们就可使用键盘在黑框框中键入咱们想要输入的MySQL语句,输入完了以后就能够点击回车键将该语句看成请求发送到服务器,但是客户端发送的语句(本质是个字符串)究竟是采用什么字符集编码的呢?这其实涉及到应用程序和操做系统之间的交互,咱们的MySQL客户端程序实际上是一个应用程序,它从黑框框中读取数据实际上是要调用操做系统提供的读取接口。在不一样的操做系统中,调用的读取接口实际上是不一样的,咱们还得分状况讨论一下:
对于UNIX操做系统来讲
在咱们使用某个输入法软件向黑框框中输入字符时,该字符采用的编码字符集实际上是操做系统当前使用的字符集。比方说当前LC_ALL
环境变量的值为zh_CN.UTF-8
,那么意味着黑框框中的字符实际上是使用utf8字符集进行编码。稍后MySQL客户端程序将调用操做系统提供的read函数从黑框框中读取数据(其实就是所谓的从标准输入流中读取数据),所读取的数据其实就是采用utf8字符集进行编码的字节序列,稍后将该字节序列做为请求内容发送到服务器。
这样其实会产生一个问题,若是客户端的默认字符集和操做系统当前正在使用的字符集不一样,那么将产生比较尴尬的结果。比方说咱们在启动客户端是携带了--default-character-set=gbk
的启动参数,那么客户端的默认字符集将会被设置成gbk,而若是操做系统此时采用的字符集是utf8。比方说咱们的语句中包含汉字'我'
,那么客户端调用read
函数读到的字节序列实际上是0xE68891
,从而将0xE68891
发送到服务器,而服务器认为客户端发送过来的请求都是采用gbk进行编码的,这样就会产生问题(固然,这仅仅是发生乱码问题的前奏,并不意味着产生乱码,乱码只有在最后一步,也就是客户端应用程序将服务器返回的数据写到黑框框里时才会发生)。
对于Windows操做系统来讲
在Windows操做系统中,从黑框框中读取数据调用的是Windows提供的ReadConsoleW
函数。在该函数执行后,MySQL客户端会获得一个宽字符数组(其实就是一组16位的UNICODE),而后客户端须要把该宽字符数组再次转换成客户端使用的默认字符集编码的字节序列,而后才将该字节序列做为请求的内容发送到服务器。
这样在UNIX操做系统中可能产生的问题,在Windows系统中却能够避免。比方说咱们在启动客户端是携带了--default-character-set=gbk
的启动参数,那么客户端的默认字符集将会被设置成gbk,假如此时操做系统采用的字符集是utf8。比方说咱们的语句中包含汉字'我'
,那么客户端调用ReadConsoleW
函数先读到一个表明着我
字的宽字符数组,以后又将其转换为客户端的默认字符集,也就是gbk字符集编码的数据0xCED2
,而后将0xCED2
发送到服务器。此时服务器也认为客户端发送过来的请求就是采用gbk进行编码的,这样就彻底正确了~
服务器接收到到的请求本质上就是一个字节序列,服务器将其看做是采用系统变量character_set_client
表明的字符集进行编码的字节序列。character_set_client
是一个SESSION级别的系统变量,也就是说每一个客户端和服务器创建链接后,服务器都会为该客户端维护一个单独的character_set_client
变量,每一个客户端在登陆服务器的时候都会将客户端的默认字符集通知给服务器,而后服务器设置该客户端专属的character_set_client
。
咱们可使用SET命令单独修改character_set_client
对应的值,就像这样:
SET character_set_client=gbk;
复制代码
须要注意的是,character_set_client
对应的字符集必定要包含请求中的字符,比方说咱们把character_set_client
设置成ascii
,而请求中发送了一个汉字'我'
,将会发生这样的事情:
mysql> SET character_set_client=ascii;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+------------------------------------------------------+
| Variable_name | Value |
+--------------------------+------------------------------------------------------+
| character_set_client | ascii |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/Cellar/mysql/5.7.21/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+
8 rows in set (0.00 sec)
mysql> SELECT '我';
+-----+
| ??? |
+-----+
| ??? |
+-----+
1 row in set, 1 warning (0.00 sec)
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
Level: Warning
Code: 1300
Message: Invalid ascii character string: '\xE6\x88\x91'
1 row in set (0.00 sec)
复制代码
如图所示,最后提示了'E六、8八、91'
并非正确的ascii字符。
小贴士: 能够将character_set_client设置为latin1,看看还会不会报告WARNINGS,以及为何~
服务器在处理请求时会将请求中的字符再次转换为一种特定的字符集,该字符集由系统变量character_set_connection
表示,该系统变量也是SESSION级别的。每一个客户端在登陆服务器的时候都会将客户端的默认字符集通知给服务器,而后服务器设置该客户端专属的character_set_connection
。
不过咱们以后能够经过SET命令单独修改这个character_set_connection
系统变量。比方说客户端发送给服务器的请求中包含字节序列0xE68891
,而后服务器针对该客户端的系统变量character_set_client
为utf8
,那么此时服务器就知道该字节序列实际上是表明汉字'我'
,若是此时服务器针对该客户端的系统变量character_set_connection
为gbk,那么在计算机内部还须要将该字符转换为采用gbk字符集编码的形式,也就是0xCED2
。
有同窗可能会想这一步有点儿像脱了裤子放屁的意思,可是你们请考虑下边这个查询语句:
mysql> SELECT 'a' = 'A';
复制代码
请问你们这个查询语句的返回结果应该是TRUE仍是FALSE?其实结果是不肯定。这是由于咱们并不知道比较两个字符串的大小到底比的是什么!咱们应该从两个方面考虑:
考虑一:这些字符串是采用什么字符集进行编码的呢?
考虑二:在咱们肯定了编码这些字符串的字符集以后,也就意味着每一个字符串都会映射到一个字节序列,那么咱们怎么比较这些字节序列呢,是直接比较它们二进制的大小,仍是有别的什么比较方式?比方说'a'
和'A'
在utf8字符集下的编码分别为0x61
和0x41
,那么'a' = 'A'
是应该直接比较0x61
和0x41
的大小呢,仍是将0x61
减去32以后再比较大小呢?其实这两种比较方式均可以,每一种比较方式咱们都称做一种比较规则
(英文名:collation
)。
MySQL
中支持若干种字符集,咱们可使用SHOW CHARSET
命令查看,以下图所示(太多了,只展现几种,具体本身运行一下该命令):
mysql> SHOW CHARSET;
+----------+---------------------------------+---------------------+--------+
| Charset | Description | Default collation | Maxlen |
+----------+---------------------------------+---------------------+--------+
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
| latin1 | cp1252 West European | latin1_swedish_ci | 1 |
| latin2 | ISO 8859-2 Central European | latin2_general_ci | 1 |
| ascii | US ASCII | ascii_general_ci | 1 |
| gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 |
| gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 |
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
| utf16 | UTF-16 Unicode | utf16_general_ci | 4 |
| utf16le | UTF-16LE Unicode | utf16le_general_ci | 4 |
| utf32 | UTF-32 Unicode | utf32_general_ci | 4 |
| binary | Binary pseudo charset | binary | 1 |
| gb18030 | China National Standard GB18030 | gb18030_chinese_ci | 4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.04 sec)
复制代码
其中每一种字符集又对应着若干种比较规则,咱们以utf8字符集为例(太多了,也只展现几个):
mysql> SHOW COLLATION WHERE Charset='utf8';
+--------------------------+---------+-----+---------+----------+---------+
| Collation | Charset | Id | Default | Compiled | Sortlen |
+--------------------------+---------+-----+---------+----------+---------+
| utf8_general_ci | utf8 | 33 | Yes | Yes | 1 |
| utf8_bin | utf8 | 83 | | Yes | 1 |
| utf8_unicode_ci | utf8 | 192 | | Yes | 8 |
| utf8_icelandic_ci | utf8 | 193 | | Yes | 8 |
| utf8_latvian_ci | utf8 | 194 | | Yes | 8 |
| utf8_romanian_ci | utf8 | 195 | | Yes | 8 |
+--------------------------+---------+-----+---------+----------+---------+
27 rows in set (0.00 sec)
复制代码
其中utf8_general_ci
是utf8字符集默认的比较规则,在这种比较规则下是不区分大小写的,不过utf8_bin
这种比较规则就是区分大小写的。
在咱们将请求中的字节序列转换为character_set_connection
对应的字符集编码的字节序列后,也要配套一个对应的比较规则,这个比较规则就由collation_connection
系统变量来指定。咱们如今经过SET命令来修改一下和
collation_connection
的值分别设置为utf8
和utf8_general_ci
,而后比较一下'a'
和'A'
:
mysql> SET character_set_connection=utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> SET collation_connection=utf8_general_ci;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT 'a' = 'A';
+-----------+
| 'a' = 'A' |
+-----------+
| 1 |
+-----------+
1 row in set (0.00 sec)
复制代码
能够看到在这种状况下这两个字符串就是相等的。
咱们如今经过SET命令来修改一下和
collation_connection
的值分别设置为utf8
和utf8_bin
,而后比较一下'a'
和'A'
:
mysql> SET character_set_connection=utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> SET collation_connection=utf8_bin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT 'a' = 'A';
+-----------+
| 'a' = 'A' |
+-----------+
| 0 |
+-----------+
1 row in set (0.00 sec)
复制代码
能够看到在这种状况下这两个字符串就是不相等的。
固然,若是咱们并不须要单独指定将请求中的字符串采用何种字符集以及比较规则的话,并不用太关心character_set_connection
和collation_connection
设置成啥,不过须要注意一点,就是character_set_connection
对应的字符集必须包含请求中的字符。
为了故事的顺利发展,咱们先建立一个表:
CREATE TABLE t (
c VARCHAR(100)
) ENGINE=INNODB CHARSET=utf8;
复制代码
而后向这个表插入一条记录:
INSERT INTO t VALUE('我');
复制代码
如今这个表中的数据就以下所示:
mysql> SELECT * FROM t;
+------+
| c |
+------+
| 我 |
+------+
1 row in set (0.00 sec)
复制代码
咱们能够看到该表中的字段实际上是使用utf8
字符集编码的,因此底层存放格式是:0xE68891
,将它读出后须要发送到客户端,是否是直接将0xE68891
发送到客户端呢?这可不必定,这个取决于character_set_result
系统变量的值,该系统变量也是一个SESSION级别的变量。服务器会将该响应转换为character_set_result
系统变量对应的字符集编码后的字节序列发送给客户端。每一个客户端在登陆服务器的时候都会将客户端的默认字符集通知给服务器,而后服务器设置该客户端专属的character_set_result
。
咱们也可使用SET命令来设置character_set_result
的值。不过也须要注意,character_set_result
对应的字符集应该包含响应中的字符。
这里再强调一遍,character_set_client
、character_set_connection
和character_set_result
这三个系统变量是服务器的系统变量,每一个客户端在与服务器创建链接后,服务器都会为这个链接维护这三个变量,如图所示(咱们假设链接1的这三个变量均为utf8
,链接1的这三个变量均为gbk
,链接1的这三个变量均为ascii
,):
character_set_client
、
character_set_connection
和
character_set_result
这三个系统变量应该和客户端的默认字符集相同,
SET names
命令能够一次性修改这三个系统变量:
SET NAMES 'charset_name'
复制代码
该语句和下边三个语句等效:
SET character_set_client = charset_name;
SET character_set_results = charset_name;
SET character_set_connection = charset_name;
复制代码
不过这里须要你们特别注意,SET names
语句并不会改变客户端的默认字符集!
客户端收到的响应其实仍然是一个字节序列。客户端是如何将这个字节序列写到黑框框中的呢,这又涉及到应用程序和操做系统之间的一次交互。
对于UNIX操做系统来讲,MySQL客户端向黑框框中写入数据使用的是操做系统提供的fputs
、putc
或者fwrite
函数,这些函数基本上至关于直接就把接收到的字节序列写到了黑框框中(请注意咱们用词:'基本上至关于'
,其实内部还会作一些工做,可是咱们这里就不想再关注这些细节了)。此时若是该字节序列实际的字符集和黑框框展现字符所使用的字符集不一致的话,就会发生所谓的乱码(你们注意,这个时候和操做系统当前使用的字符集没啥关系)。
比方说咱们在启动MySQL客户端的时候使用了--default-character-set=gbk
的启动参数,那么服务器的character_set_result
变量就是gbk。而后再执行SELECT * FROM t
语句,那么服务器就会将字符'我'
的gbk编码,也就是0xCDE2
发送到客户端,客户端直接把这个字节序列写到黑框框中,若是黑框框此时采用utf8字符集展现字符,那天然就会发生乱码。
对于Windows操做系统来讲,MySQL客户端向黑框框中写入数据使用的是操做系统提供的WriteConsoleW
函数,该函数接收一个宽字符数组,因此MySQL客户端调用它的时候须要显式地将它从服务器收到的字节序列按照客户端默认的字符集转换成一个宽字符数组。正由于这一步骤的存在,因此能够避免上边提到的一个问题。
比方说咱们在启动MySQL客户端的时候使用了--default-character-set=gbk
的启动参数,那么服务器的character_set_result
变量就是gbk。而后再执行SELECT * FROM t
语句,那么服务器就会将字符'我'
的gbk编码,也就是0xCDE2
发送到客户端,客户端将这个字节序列先从客户端默认字符集,也就是gbk的编码转换成一个宽字符数组,而后再调用WriteConsoleW
函数写到黑框框,黑框框天然能够把它显示出来。
好了,介绍了各个步骤中涉及到的各类字符集,你们估计也看的眼花缭乱了,下边总结一下咱们遇到乱码的时候应该如何分析,而不是胡子眉毛一把抓,随便百度一篇文章,而后修改某个参数,运气好修改了以后改对了,运气很差改了一天也改很差。知其然也要知其因此然,在学习了本篇文章后,你们必定要有节奏的去分析乱码问题:
我使用的是什么操做系统
对于UNIX系统用户来讲,要搞清楚我使用的黑框框究竟是使用什么字符集展现字符,就像是iTerm2
中的character encoding
属性:
locale
命令查看: 王大爷喊你输入呢,跟这儿>locale
LANG=""
LC_COLLATE="zh_CN.UTF-8"
LC_CTYPE="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_ALL="zh_CN.UTF-8"
王大爷喊你输入呢,跟这儿>
复制代码
没有什么特别极端的特殊需求的话,必定要保证上述两个字符集是相同的,不然可能连汉字都输入不进去!
对于Windows用户来讲
搞清楚本身使用的黑框框的代码页是什么,也就是操做系统当前使用的字符集是什么。
搞清楚客户端的默认字符集是什么
启动MySQL客户端的时候有没有携带--default-character-set
参数,若是携带了,那么客户端默认字符集就以该参数指定的值为准。不然分析本身操做系统当前使用的字符集是什么。
搞清楚客户端发送请求时是以什么字符集编码请求的
对于UNIX系统来讲,咱们能够认为请求就是采用操做系统当前使用的字符集进行编码的。
对于Windows系统来讲,咱们能够认为请求就是采用客户端默认字符集进行编码的。
经过执行SHOW VARIABLES LIKE 'character%'
命令搞清楚:
character_set_client
:服务器是怎样认为客户端发送过来的请求是采用何种字符集编码的character_set_connection
:服务器在运行过程当中会采用何种字符集编码请求中的字符character_set_result
:服务器会将响应使用何种字符集编码后再发送给客户端的客户端收到响应以后:
对于服务器发送过来的字节序列来讲:
在UNIX操做系统上,能够认为会把该字节序列直接写到黑框框里。此时应该搞清楚咱们的黑框框究竟是采用何种字符集展现数据。
在Windows操做系统上,该字节序列会被认为是由客户端字符集编码的数据,而后再转换成宽字符数组写入到黑框框中。
请认真分析上述的每个步骤,而后发出惊呼:小样,不就是个乱码嘛,还治不了个你!
想轻松的了解更多MySQL进阶内容,请看小册:《MySQL是怎样运行的:从根儿上理解MySQL》的连接。
本文首发于公众号「咱们都是小青蛙」。
写文章挺累的,有时候你以为阅读挺流畅的,那实际上是背后无数次修改的结果。若是你以为不错请帮忙转发一下,万分感谢~ 这里是个人公众号「咱们都是小青蛙」,里边有更多技术干货,时不时扯一下犊子,欢迎关注: