mysql使用utf8mb4經驗吐血總結

ACMUG徵集原創技術文章。詳情請添加 A_CMUG或者掃描文末二維碼關注咱們的微信公衆號。有獎徵稿,請發送稿件至:acmug@acmug.com。 
3306現金有獎徵稿說明: 知識無價,勞動有償,ACMUG特約撰稿人有獎回報計劃(修訂版)php

做者簡介: 周曉html

 

網絡經常使用id seanlook 。之前在TP-LINK作了2年Oracle DBA,後來專職作MySQL了。平時在工做中遇到的些問題和處理經驗,有空會寫寫放在本身的網站上 http://seanlook.com java

01python

utf8 與 utf8mb4 異同mysql

先看 官方手冊  https://dev.mysql.com/doc/refman/5.6/en/charset-unicode-utf8mb4.html  的說明:linux

The character set named utf8 uses a maximum of three bytes per character and contains only BMP characters. The utf8mb4 character set uses a maximum of four bytes per character supports supplementary characters:c++

  • For a BMP character, utf8 and utf8mb4 have identical storage characteristics: same code values, same encoding, same length.算法

  • For a supplementary character, utf8 cannot store the character at all, whereas utf8mb4 requires four bytes to store it. Because utf8 cannot store the character at all, you have no supplementary characters in utf8 columns and need not worry about converting characters or losing data when upgrading utf8 data from older versions of MySQL.sql

MySQL在 5.5.3 以後增長了 utf8mb4 字符編碼,mb4即 most bytes 4。簡單說 utf8mb4 是 utf8 的超集並徹底兼容utf8,可以用四個字節存儲更多的字符。數據庫

但拋開數據庫,標準的 UTF-8 字符集編碼是能夠用 1~4 個字節去編碼21位字符,這幾乎包含了是世界上全部能看見的語言了。然而在MySQL裏實現的utf8最長使用3個字節,也就是隻支持到了 Unicode 中的 基本多文本平面 (U+0000至U+FFFF),包含了控制符、拉丁文,中、日、韓等絕大多數國際字符,但並非全部,最多見的就算如今手機端經常使用的表情字符 emoji和一些不經常使用的漢字,如 「墅」 ,這些須要四個字節才能編碼出來。

注:QQ裏面的內置的表情不算,它是經過特殊映射到的一個gif圖片。通常輸入法自帶的就是。

也就是當你的數據庫裏要求可以存入這些表情或寬字符時,能夠把字段定義爲 utf8mb4,同時要注意鏈接字符集也要設置爲utf8mb4,不然在 嚴格模式 下會出現  Incorrect string value: /xF0/xA1/x8B/xBE/xE5/xA2… for column 'name' 這樣的錯誤,非嚴格模式下此後的數據會被截斷。

提示:另一種可以存儲emoji的方式是,不關心數據庫表字符集,只要鏈接字符集使用 latin1,但相信我,你絕對不想這個幹,一是這種字符集混用管理極不規範,二是存儲空間被放大(讀者能夠想下爲何)。

02

utf8mb4_ unicode_ ci 與 utf8mb4_ general_ ci 如何選擇

字符除了須要存儲,還須要排序或比較大小,涉及到與編碼字符集對應的 排序字符集(collation)。ut8mb4對應的排序字符集經常使用的有 utf8mb4_unicode_ci 、 utf8mb4_general_ci ,到底採用哪一個在 stackoverflow 上有個討論, What’s the difference between utf8_general_ci and utf8_unicode_ci

主要從排序準確性和性能兩方面看:

  • 準確性

    utf8mb4_unicode_ci 是基於標準的Unicode來排序和比較,可以在各類語言之間精確排序

    utf8mb4_general_ci 沒有實現Unicode排序規則,在遇到某些特殊語言或字符是,排序結果可能不是所指望的。

    可是在絕大多數狀況下,這種特殊字符的順序必定要那麼精確嗎。好比Unicode把 ß 、 Œ 當成 ss 和 OE 來看;而general會把它們當成 s 、 e ,再如 ÀÁÅåāă 各自都與  A 相等。

  • 性能

    utf8mb4_general_ci 在比較和排序的時候更快

    utf8mb4_unicode_ci 在特殊狀況下,Unicode排序規則爲了可以處理特殊字符的狀況,實現了略微複雜的排序算法。

    可是在絕大多數狀況下,不會發生此類複雜比較。general理論上比Unicode可能快些,但相比如今的CPU來講,它遠遠不足以成爲考慮性能的因素,索引涉及、SQL設計纔是。 我我的推薦是 utf8mb4_unicode_ci ,未來 8.0 裏也極有可能使用變爲默認的規則。

這也從另外一個角度告訴咱們,不要可能產生亂碼的字段做爲主鍵或惟一索引。我遇到過一例,以 url 來做爲惟一索引,可是它記錄的有多是亂碼,致使後來想把它們修復就特別麻煩。

03

怎麼從utf8轉換爲utf8mb4

3.1 「僞」轉換

若是你的表定義和鏈接字符集都是utf8,那麼直接在你的表上執行

ALTER TABLE tbl_name CONVERT TO CHARACTER SET utf8mb4;

則可以該表上全部的列的character類型變成 utf8mb4,表定義的默認字符集也會修改。鏈接的時候須要使用 set names utf8mb4 即可以插入四字節字符。(若是依然使用 utf8 鏈接,只要不出現四字節字符則徹底沒問題)。

上面的 convert 有兩個問題,一是它不能ONLINE,也就是執行以後全表禁止修改,有關這方面的討論見 mysql 5.6 原生Online DDL解析 ;二是,它可能會自動該表字段類型定義,如  VARCHAR 被轉成 MEDIUMTEXT ,能夠經過 MODIFY 指定類型爲原類型。

另外 ALTER TABLE tbl_name DEFAULT CHARACTER SET utf8mb4 這樣的語句就不要隨便執行了,特別是當表本來不是utf8時,除非表是空的或者你確認表裏只有拉丁字符,不然正常和亂的就混在一塊兒了。

最重要的是,你鏈接時使用的latin1字符集寫入了歷史數據,表定義是latin1或utf8,不要指望經過  ALTER ... CONVERT ... 可以讓你達到用utf8讀取歷史中文數據的目的,沒卵用,老老實實作邏輯dump。因此我才叫它「僞」轉換

3.2 character-set-server

一旦你決定使用utf8mb4,強烈建議你要修改服務端 character-set-server=utf8mb4,不一樣的語言對它的處理方法不同,c++, php, python能夠設置character-set,但java驅動依賴於 character-set-server 選項,後面有介紹。

同時還要謹慎一些特殊選項,如 遇到騰訊雲CDB鏈接字符集設置一個坑 。我的不建議設置全局  init_connect 。

04

key 768 long 錯誤

字符集從utf8轉到utf8mb4以後,最容易引發的就是索引鍵超長的問題。

對於錶行格式是 COMPACT 或  REDUNDANT ,InnoDB有單個索引最大字節數 768 的限制,而字段定義的是能存儲的字符數,好比  VARCHAR(200) 表明可以存200個漢字,索引定義是字符集類型最大長度算的,即 utf8 maxbytes=3, utf8mb4 maxbytes=4,算下來utf8和utf8mb4兩種狀況的索引長度分別爲600 bytes和800bytes,後者超過了768,致使出錯: Error 1071: Specified key was too long; max key length is 767 bytes 。

COMPRESSED 和 DYNAMIC 格式不受限制,但也依然不建議索引太長,太浪費空間和cpu搜索資源。

若是已有定義超過這個長度的,可加上前綴索引,若是暫不能加上前綴索引(像惟一索引),可把該字段的字符集改回utf8或latin1。

可是,(  敲黑板啦,很重要  ),要防止出現  Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8mb4_general_ci,COERCIBLE) for operation '=' 錯誤:鏈接字符集使用utf8mb4,但 SELECT/UPDATE where條件有utf8類型的列,且條件右邊存在不屬於utf8字符,就會觸發該異常。表示踩過這個坑。

再多加一個友好提示:EXPLAIN 結果裏面的 key_len 指的搜索索引長度,單位是bytes,並且是以字符集支持的單字符最大字節數算的,這也是爲何 INDEX_LENGTH 膨脹厲害的一個緣由。

05

C/C++ 內存空間分配問題

這是咱們這邊的開發遇到的一個棘手的問題。C或C++鏈接MySQL使用的是linux系統上的 libmysqlclient 動態庫,程序獲取到數據以後根據自定義的一個網絡協議,按照mysql字段定義的固定字節數來傳輸數據。從utf8轉utf8mb4以後,c++裏面針對character單字符內存空間分配,從3個增長到4個,引發異常。

這個問題實際上是想說明,使用utf8mb4以後,官方建議儘可能用 varchar 代替 char,這樣能夠減小固定存儲空間浪費(關於char與varchar的選擇,可參考 這裏 )。但開發設計表時 varchar 的大小不能隨意加大,它雖然是變長的,但客戶端在定義變量來獲取數據時,是以定義的爲準,而非實際長度。按需分配,避免程序使用過多的內存。

06

java驅動使用

Java語言裏面所實現的UTF-8編碼就是支持4字節的,因此不須要配置 mb4 這樣的字眼,但若是從MySQL讀寫emoji,MySQL驅動版本要在 5.1.13 及以上版本,數據庫鏈接依然是  characterEncoding=UTF-8 。

但還沒完,遇到一個大坑。 官方手冊 裏還有這麼一段話:

Connector/J did not support utf8mb4  for servers  5.5 .2 and newer.

Connector/J now auto-detects servers configured with character_set_server=utf8mb4  or treats  the Java encoding utf -8 passed

using characterEncoding=...  as utf8mb4  in the SET NAMES= calls  it makes when establishing  the connection. (Bug  #54175)

意思是,java驅動會自動檢測服務端 character_set_server 的配置,若是爲utf8mb4,驅動在創建鏈接的時候設置  SET NAMES utf8mb4 。然而其餘語言沒有依賴於這樣的特性。

07

主從複製錯誤

這個問題沒有遇到,只是看官方文檔有提到,曾經也看到過相似的技術文章。

大概就是從庫的版本比主庫的版本低,致使有些字符集不支持;或者人工修改了從庫上的表或字段的字符集定義,都有可能引發異常。

08

join 查詢問題

這個問題是以前在姜承堯老師公衆號看到的一篇文章 「MySQL表字段字符集不一樣致使的索引失效問題」,本身也驗證了一下,的確會有問題:

CREATE TABLE t1 (

f_id varchar ( 20 )  NOT NULL ,

f_action char ( 25 )  NOT NULL DEFAULT '' COMMENT '' ,

PRIMARY KEY ( `f_id` ),

) ENGINE = InnoDB DEFAULT CHARSET =utf8 ROW_FORMAT=DYNAMIC;

CREATE TABLE t1_copy_mb4 (

f_id varchar ( 20 )  CHARACTER SET utf8mb4  NOT NULL ,

f_action char ( 25 )  NOT NULL DEFAULT '' COMMENT '' ,

PRIMARY KEY ( `f_id` ),

) ENGINE = InnoDB DEFAULT CHARSET =utf8 ROW_FORMAT=DYNAMIC;

1. EXPLAIN   extended   select  *  from  t1  INNER   JOIN  t1_copy_mb4 t2  on  t1.f_id=t2.f_id  where  t1.f_id= '421036' ;

2. EXPLAIN   extended   select  *  from  t1  INNER   JOIN  t1_copy_mb4 t2  on  t1.f_id=t2.f_id  where  t2.f_id= '421036' ;

對應上面1,2 的截圖:

其中 2 的warnings 有convert:

  • (convert( t1.f_id using utf8mb4) = ‘421036’)

官網能找到這一點解釋的仍是開頭那個地址:

Similarly, the following comparison in the  WHERE clause works according  to the collation  of utf8mb4_col:

SELECT *  FROM utf8_tbl, utf8mb4_tbl

WHERE utf8_tbl.utf8_col = utf8mb4_tbl.utf8mb4_col;

只是索引失效發生在utf8mb4列 在條件左邊。(關於MySQL的隱式類型轉換,見 這裏 )。

09

參考

  • https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-conversion.html

  • http://forums.mysql.com/read.php?103,187048,188748#msg-188748

  • Why are we using utf8mb4_general_ci and not utf8mb4_unicode_ci?

  • How to support full Unicode in MySQL databases

  • 10分鐘學會理解和解決MySQL亂碼問題

注:ACMUG收錄技術文章版權屬於原做者本人全部。若有疑問,請聯繫做者。

看完轉發,手留餘香。關注咱們,一塊兒進步。

關注ACMUG公衆號,參與社區活動,交流開源技術,分享學習心得,一塊兒共同進步。

相關文章
相關標籤/搜索