現(xiàn)象
在使用MySQL客戶端書寫SQL語句的時(shí)候,我們可以在字符串前邊加_charset_name的符號(hào),其中的charset_name對(duì)應(yīng)著某個(gè)具體的字符集,廢話不多說,先寫兩個(gè)例子看一下:
mysql> SELECT _utf8'我'; +-----+ | 我 | +-----+ | 我 | +-----+ 1 row in set (0.04 sec) mysql> SELECT _gbk'我'; +-----+ | 鎴 | +-----+ | 鎴 | +-----+ 1 row in set, 1 warning (0.02 sec)
可以看到第一個(gè)查詢結(jié)果正常,第二個(gè)查詢出現(xiàn)了亂碼。為什么呢?下邊細(xì)細(xì)道來。
原因
我們知道MySQL是一個(gè)C/S架構(gòu)的軟件,可以有很多客戶端連接到服務(wù)器進(jìn)行交互。客戶端發(fā)送給服務(wù)器的請(qǐng)求以及服務(wù)器發(fā)送給客戶端的響應(yīng)本質(zhì)上都是一個(gè)二進(jìn)制的字節(jié)串,每當(dāng)我們從客戶端發(fā)送一個(gè)請(qǐng)求到服務(wù)器,服務(wù)器處理完成之后再把響應(yīng)返回給客戶端的過程其實(shí)發(fā)生了很多字符集轉(zhuǎn)換過程。
- 首先請(qǐng)求會(huì)被MySQL客戶端編碼為字節(jié)序列之后通過網(wǎng)絡(luò)傳輸?shù)椒?wù)器。對(duì)于MySQL自帶的客戶端來說,這個(gè)編碼過程使用的字符集和我們使用的操作系統(tǒng)的默認(rèn)字符集是一樣的,類Unix系統(tǒng)的默認(rèn)字符集就是utf8,windows系統(tǒng)的默認(rèn)字符集就是gbk。
- 服務(wù)器收到字節(jié)序列請(qǐng)求之后,會(huì)認(rèn)為該字節(jié)串是按照character_set_client系統(tǒng)變量編碼的,之后將其從character_set_client轉(zhuǎn)換到character_set_connection,之后進(jìn)行更深入的處理。
- 最后再將響應(yīng)發(fā)送到客戶端的時(shí)候,又會(huì)按照character_set_results進(jìn)行編碼。
- 客戶端收到響應(yīng)字節(jié)串之后,按照本客戶端規(guī)定的字符集進(jìn)行解碼。對(duì)于MySQL自帶的客戶端來說,這個(gè)解碼過程使用的字符集和我們使用的操作系統(tǒng)的默認(rèn)字符集是一樣的,類Unix系統(tǒng)的默認(rèn)字符集就是utf8,Windows系統(tǒng)的默認(rèn)字符集就是gbk。
總結(jié)一下這幾個(gè)涉及到的通信字符集系統(tǒng)變量:
系統(tǒng)變量描述character_set_client服務(wù)器解碼請(qǐng)求時(shí)使用的字符集character_set_connection服務(wù)器處理請(qǐng)求時(shí)會(huì)把請(qǐng)求字符串從character_set_client轉(zhuǎn)為character_set_connectioncharacter_set_results服務(wù)器向客戶端返回?cái)?shù)據(jù)時(shí)使用的字符集
現(xiàn)在我的系統(tǒng)中的這幾個(gè)系統(tǒng)變量的值都是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前綴,意味著禁止服務(wù)器將后續(xù)字節(jié)從character_set_client轉(zhuǎn)換到character_set_connection,而是默認(rèn)使用_charset_name代表的字符集作為它后續(xù)字節(jié)的字符集。比方說:
mysql> SELECT _gbk'我'; +-----+ | 鎴 | +-----+ | 鎴 | +-----+ 1 row in set, 1 warning (0.02 sec)
我現(xiàn)在使用的是macOS操作系統(tǒng),所以
- 客戶端發(fā)送請(qǐng)求時(shí)會(huì)將字符'我'按照utf8進(jìn)行編碼,也就是:0xE68891。
- 服務(wù)器收到請(qǐng)求后發(fā)現(xiàn)有前綴_gbk,則不會(huì)將其后邊的字節(jié)0xE68891進(jìn)行從character_set_client到character_set_connection的轉(zhuǎn)換,而是直接把0xE68891認(rèn)為是某個(gè)字符串由gbk編碼后得到的字節(jié)序列。
- 然后再把上述0xE68891從gbk轉(zhuǎn)換為character_set_results,也就是utf8。0xE688在gbk中代表漢字'鎴',而0x91無法解碼(我們可以看到上述查詢結(jié)果中有1個(gè)warning)。我們緊接著上邊的查詢語句執(zhí)行一下SHOW WARNINGS:
mysql> SHOW WARNINGSG *************************** 1. row *************************** Level: Warning Code: 1300 Message: Invalid gbk character string: '91' 1 row in set (0.01 sec)
之后將漢字'鎴'再按照utf8進(jìn)行編碼,得到的結(jié)果就是E98EB4,把它發(fā)送到客戶端。
- 客戶端收到之后再解碼到屏幕上,解碼也使用utf8字符集,所以就出現(xiàn)了鎴。
擴(kuò)展
如果在我的機(jī)器上我執(zhí)行SELECT LENGTH(_gbk '我')會(huì)得到什么結(jié)果呢(LENGTH函數(shù)用來統(tǒng)計(jì)某個(gè)字符串共占用多少字節(jié))?有很多小伙伴不經(jīng)思考,脫口而出:2!哈哈,我們看一下結(jié)果驗(yàn)證一下:
mysql> SELECT LENGTH(_gbk '我'); +--------------------+ | LENGTH(_gbk '我') | +--------------------+ | 3 | +--------------------+ 1 row in set, 1 warning (0.01 sec)
WTH?竟然是3?其實(shí)再回想一下我們上邊所說的,因?yàn)?#39;我'前邊加了_gbk,所以不會(huì)經(jīng)歷從character_set_client到character_set_connection的轉(zhuǎn)換過程,而是直接把0xE68891當(dāng)作是一個(gè)采用gbk編碼的字節(jié)串。這個(gè)字節(jié)串中有3個(gè)字節(jié),當(dāng)然結(jié)果就返回3了(雖然0x91這個(gè)字節(jié)在gbk字符集中是無效的,可以看到上邊查詢語句中也給出了Warning)。
思考
如果我現(xiàn)在不使用基于macOS操作系統(tǒng)的客戶端,而采用基于Windows操作系統(tǒng)的客戶端來發(fā)送請(qǐng)求,那么下邊的語句的返回結(jié)果將會(huì)是什么呢:
SELECT LENGTH(_utf8 '我');