不得不注意!那些容易被忽視的MySQL字符集問題?

2019-12-05     老男孩的成長之路


現象

在使用MySQL客戶端書寫SQL語句的時候,我們可以在字符串前邊加_charset_name的符號,其中的charset_name對應著某個具體的字符集,廢話不多說,先寫兩個例子看一下:

mysql> SELECT _utf8'我';
+-----+
| 我 |
+-----+
| 我 |
+-----+
1 row in set (0.04 sec)

mysql> SELECT _gbk'我';
+-----+
| 鎴 |
+-----+
| 鎴 |
+-----+
1 row in set, 1 warning (0.02 sec)

可以看到第一個查詢結果正常,第二個查詢出現了亂碼。為什麼呢?下邊細細道來。

原因

我們知道MySQL是一個C/S架構的軟體,可以有很多客戶端連接到伺服器進行交互。客戶端發送給伺服器的請求以及伺服器發送給客戶端的響應本質上都是一個二進位的位元組串,每當我們從客戶端發送一個請求到伺服器,伺服器處理完成之後再把響應返回給客戶端的過程其實發生了很多字符集轉換過程。

  • 首先請求會被MySQL客戶端編碼為位元組序列之後通過網絡傳輸到伺服器。對於MySQL自帶的客戶端來說,這個編碼過程使用的字符集和我們使用的作業系統的默認字符集是一樣的,類Unix系統的默認字符集就是utf8,Windows系統的默認字符集就是gbk。
  • 伺服器收到位元組序列請求之後,會認為該位元組串是按照character_set_client系統變量編碼的,之後將其從character_set_client轉換到character_set_connection,之後進行更深入的處理。
  • 最後再將響應發送到客戶端的時候,又會按照character_set_results進行編碼。
  • 客戶端收到響應位元組串之後,按照本客戶端規定的字符集進行解碼。對於MySQL自帶的客戶端來說,這個解碼過程使用的字符集和我們使用的作業系統的默認字符集是一樣的,類Unix系統的默認字符集就是utf8,Windows系統的默認字符集就是gbk。

總結一下這幾個涉及到的通信字符集系統變量:

系統變量描述character_set_client伺服器解碼請求時使用的字符集character_set_connection伺服器處理請求時會把請求字符串從character_set_client轉為character_set_connectioncharacter_set_results伺服器向客戶端返回數據時使用的字符集

現在我的系統中的這幾個系統變量的值都是utf8:

mysql> SHOW VARIABLES LIKE 'character_set_client';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| character_set_client | utf8 |
+----------------------+-------+
1 row in set (0.24 sec)

mysql> SHOW VARIABLES LIKE 'character_set_connection';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| character_set_connection | utf8 |
+--------------------------+-------+
1 row in set (0.25 sec)

mysql> SHOW VARIABLES LIKE 'character_set_results';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| character_set_results | utf8 |
+-----------------------+-------+
1 row in set (0.30 sec)

如果我們使用了_charset_name前綴,意味著禁止伺服器將後續位元組從character_set_client轉換到character_set_connection,而是默認使用_charset_name代表的字符集作為它後續位元組的字符集。比方說:

mysql> SELECT _gbk'我';
+-----+
| 鎴 |
+-----+
| 鎴 |
+-----+
1 row in set, 1 warning (0.02 sec)

我現在使用的是macOS作業系統,所以

  • 客戶端發送請求時會將字符'我'按照utf8進行編碼,也就是:0xE68891。
  • 伺服器收到請求後發現有前綴_gbk,則不會將其後邊的位元組0xE68891進行從character_set_client到character_set_connection的轉換,而是直接把0xE68891認為是某個字符串由gbk編碼後得到的位元組序列。
  • 然後再把上述0xE68891從gbk轉換為character_set_results,也就是utf8。0xE688在gbk中代表漢字'鎴',而0x91無法解碼(我們可以看到上述查詢結果中有1個warning)。我們緊接著上邊的查詢語句執行一下SHOW WARNINGS:
mysql> SHOW WARNINGS\\G
*************************** 1. row ***************************
Level: Warning
Code: 1300
Message: Invalid gbk character string: '91'
1 row in set (0.01 sec)

之後將漢字'鎴'再按照utf8進行編碼,得到的結果就是E98EB4,把它發送到客戶端。

  • 客戶端收到之後再解碼到螢幕上,解碼也使用utf8字符集,所以就出現了鎴。

擴展

如果在我的機器上我執行SELECT LENGTH(_gbk '我')會得到什麼結果呢(LENGTH函數用來統計某個字符串共占用多少位元組)?有很多小夥伴不經思考,脫口而出:2!哈哈,我們看一下結果驗證一下:

mysql> SELECT LENGTH(_gbk '我');
+--------------------+
| LENGTH(_gbk '我') |
+--------------------+
| 3 |
+--------------------+
1 row in set, 1 warning (0.01 sec)

WTH?竟然是3?其實再回想一下我們上邊所說的,因為'我'前邊加了_gbk,所以不會經歷從character_set_client到character_set_connection的轉換過程,而是直接把0xE68891當作是一個採用gbk編碼的位元組串。這個位元組串中有3個位元組,當然結果就返回3了(雖然0x91這個位元組在gbk字符集中是無效的,可以看到上邊查詢語句中也給出了Warning)。

思考

如果我現在不使用基於macOS作業系統的客戶端,而採用基於Windows作業系統的客戶端來發送請求,那麼下邊的語句的返回結果將會是什麼呢:

SELECT LENGTH(_utf8 '我');

文章來源: https://twgreatdaily.com/zh-tw/wVP84W4BMH2_cNUgFHt5.html