本文轉自 https://blog.csdn.net/czh500/article/details/86665509。
set names charset_name (如 set names utf8mb4)是以下 3 個語句的快捷方式
SET character_set_client = charset_name;
SET character_set_results = charset_name;
SET character_set_connection = charset_name;
character_set_client 是指客戶端發送過來的語句的編碼;
character_set_connection 是指mysqld收到客戶端的語句後,要轉換到的編碼;
character_set_results 是指server執行語句後,返回給客戶端的數據的編碼。
對人來說,能夠理解的是各種各樣的符號,而對計算機來說,只能理解二進制,二進制和符號之間的對應關係就是編碼。不同地域國家都有自己的一套符號集合,每個都各自用一組二進制數字表示,從而形成了不同的編碼,字符集就可以看作是編碼和符號的對應關係集合。同一個二進制數在不同的字符集下可能對應完全不一樣的字符,如在GBK字符集中,C4E3
對應的是你
,而在big5字符集中對應的是斕
,而 你
在unicode中的編碼是4F60
,在Collation-Charts 這個網站有字符集和編碼對應關係圖,可以非常直觀地看到不同編碼下二進制數和符號的對應關係。
set names 設置的3個變量就是設置mysqld和客戶端通信時,mysqld應該如何解讀client發來的字符,以及返回給客戶端什麼樣的編碼。
實驗測試
-
環境
show variables like 'character%';
+--------------------------+-----------------------------------------------------------+
| Variable_name | Value |
+--------------------------+-----------------------------------------------------------+
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/mysql-5.7.20-macos10.12-x86_64/share/charsets/ |
+--------------------------+-----------------------------------------------------------+
server端的3個編碼設置都是utf8mb4。
-
建一張表作爲測試
CREATE TABLE t1(id INT, name VARCHAR(200)) engine=InnoDB;
-
默認配置測試
INSERT INTO t1 VALUES(0, '你好');
SELECT id, name, hex(name) FROM t1;
+------+--------+--------------+
| id | name | hex(name) |
+------+--------+--------------+
| 0 | 你好 | E4BDA0E5A5BD |
+------+--------+--------------+
-
case1: 只改變 character_set_client
SET character_set_client=gbk;
INSERT INTO t1 VALUES(1, '你好');
SELECT id, name, hex(name) FROM t1;
+------+-----------+--------------------+
| id | name | hex(name) |
+------+-----------+--------------------+
| 0 | 你好 | E4BDA0E5A5BD |
| 1 | 浣犲ソ | E6B5A3E78AB2E382BD |
+------+-----------+--------------------+
- 返回的數據已經亂碼了
- 數據庫裏存的確實和第一條不一樣
-
case2: 只改變 character_set_connection
SET names utf8mb4;
SET character_set_connection = gbk;
INSERT INTO t1 VALUES(2, '你好');
SELECT id, name, hex(name) FROM t1;
+------+-----------+--------------------+
| id | name | hex(name) |
+------+-----------+--------------------+
| 0 | 你好 | E4BDA0E5A5BD |
| 1 | 浣犲ソ | E6B5A3E78AB2E382BD |
| 2 | 你好 | E4BDA0E5A5BD |
+------+-----------+--------------------+
- 返回數據正確
- 數據庫存儲的和第一條一樣
-
case3: 只改變 character_set_results
SET names utf8mb4;
SET character_set_results = gbk;
INSERT INTO t1 VALUES(3, '你好');
SELECT id, name, hex(name) FROM t1;
+------+--------+--------------------+
| id | name | hex(name) |
+------+--------+--------------------+
| 0 | <C4><E3><BA><C3> | E4BDA0E5A5BD |
| 1 | 你好 | E6B5A3E78AB2E382BD |
| 2 | <C4><E3><BA><C3> | E4BDA0E5A5BD |
| 3 | <C4><E3><BA><C3> | E4BDA0E5A5BD |
+------+--------+--------------------+
- 返回數據錯誤
- 數據庫存儲的和第一條一樣
分析
我們先理下字符集在整個過程中是怎樣變化的,然後再分析上面的case
- 客戶發送請求時:
A1 客戶端發送出語句(總是以utf8)------> A2 sever收到語句解析(按character_set_client指定編碼)
|
v
A4 數據進入mysqld內部存儲<--------- A3 sever判斷是否需要轉換編碼(以character_set_connection 目標編碼)
- server返回結果時:
B1 server返回結果(按character_set_results 指定編碼) ----->B2客戶端解析編碼顯示(總是以utf8)
A3步是否需要轉換編碼,代碼中的邏輯是這樣的,在sql_yacc.yy文件中:
LEX_STRING tmp;
THD *thd= YYTHD;
const CHARSET_INFO *cs_con= thd->variables.collation_connection;
const CHARSET_INFO *cs_cli= thd->variables.character_set_client;
uint repertoire= thd->lex->text_string_is_7bit &&
my_charset_is_ascii_based(cs_cli) ?
MY_REPERTOIRE_ASCII : MY_REPERTOIRE_UNICODE30;
if (thd->charset_is_collation_connection ||
(repertoire == MY_REPERTOIRE_ASCII &&
my_charset_is_ascii_based(cs_con)))
tmp= $1;
else
{
if (thd->convert_string(&tmp, cs_con, $1.str, $1.length, cs_cli))
MYSQL_YYABORT;
}
$$= new (thd->mem_root) Item_string(tmp.str, tmp.length, cs_con,
DERIVATION_COERCIBLE,
repertoire);
if ($$ == NULL)
MYSQL_YYABORT;
如果 character_set_client
和 character_set_connection
一樣,或者當前的字符編碼是和ASCII兼容,並且都是ASCII範圍內的,就不轉換,其它情況就轉。
-
對於case1
實際上客戶端發過來是UTF8的,但A2步驟server認爲客戶端的編碼是GBK的,就按GBK來解析,同時滿足A3步驟的轉換條件,所以就誤將UTF8編碼認爲是GBK,然後又給轉成了UTF8。你好
的UTF8編碼是 E4BDA0E5A5BD
6個字節,每個字符3個字節,按GBK來解析的話,因爲GBK是固定2個字節,就認爲有3個字符,然後轉成UTF8,雖然UTF8是變長的,但是這裏的3個GBK字符按值都是要佔3個字節的,轉出來一共9個字節。所以case1看到的實際存儲的值一共9個字節,比原來的大。
在返回時,是按UTF8返回的,因爲存了3個UTF8字符,所以客戶端看到的就是3個。
-
對於case2
A2步驟沒問題,問題是出在A3,按照轉換邏輯,此時需要把UTF8轉成GBK,這裏因爲character_set_client
是正確的,所以轉換的源不會識別錯,轉換成GBK自然也不會錯,後面存儲成UTF8時,再從GBK轉成UTF8,也沒錯,因爲UTF8和GBK字符集裏都包含 ‘你’和’好’,所以相互轉換也不會出錯,只是多了2次轉換。
-
對於case3
錯在返回字符集設置的和客戶端不匹配,在返回時,server將所有字符轉成GBK的,結果客戶端一根筋的認爲是UTF8,就解析錯了。
比較有意思的是第二條記錄,即case1錯誤插進去的,顯示出來是對的。
爲什麼呢,因爲在case1中存的時候,是按 UTF8->強制解析爲GBK->然後轉爲UTF8
這個邏輯存下去的,而返回的時候,因爲server會將存的UTF8又給轉回GBK,然後客戶端又拿着這個GBK誤以爲是UTF8解析,實際上是case1的逆向過程,雖然2個方向都是錯的,最終顯示是好的,所謂的負負得正吧,哈哈。
對於case2 ,數據從客戶端進入server的時候,多做了2次轉換,最終顯示還是對的,但不是所有場景都是這樣,如下面這種
總結
character_set_client
和 character_set_results
是一定要和客戶端一致,不要依賴於負負得正,character_set_connection
設置和character_set_client
不一致,有丟失數據的風險,所以儘量也一致,總之這3個值就是要一樣,還要和客戶端一致,所以纔有了 set names 這個快捷命令。關於爲啥要有 character_set_connection
這一步轉換,筆者目前還沒看出來,以後理解了再更新,如果讀者朋友知道的話,請不吝賜教。