MySQL 5.7 版本的 UTF8 字符集調研

一、故事背景

記一次 sql_mode 非嚴格模式下的業務事故排查。當時資料庫沒有開啟 sql_mode 為嚴格模式,並且數據表的編碼是 utf8,表現為業務側的 Insert SQL 語句執行成功,但是,
查詢表記錄的時候,發現欄位的數據值缺失。示例:寫入一條有特殊字元 𝑥  的記錄,記錄裡面欄位值在 𝑥 之後的字元都丟失了。

下面是,開啟了嚴格模式:

問題原因定位到後,解決方案是,在不對資料庫做任何配置調整的前提下,業務邏輯中增加對特殊字元的檢測,過濾掉資料庫不支援的特殊字元,從而杜絕寫入數據表後出現數據缺失的事故。

那麼,哪些字元是 MySQL 不支援的嘞?由此引出本文的探討主題。

二、認識 MySQL UTF8 字符集

我們帶著兩個問題,去調研 MySQL 5.7 版本 UTF8 字符集。

2.1. MySQL 不支援的特殊字元有哪些?

PS: 這裡貼的 MySQL 官方文檔也是 5.7。

從文檔提取下關鍵資訊:

  • 在 MySQL 中 utf8 是 utf8mb3 的別名
  • utf8mb3 編碼的每個字元最多三個位元組

示例:特殊字元 𝑥 特殊字元:

可以觀察到這個字元,需要使用四個位元組編碼,因此這個字元不能被資料庫 utf8mb3 編碼支援。

說點題外話,在 Java 中 String 是 UTF-16 格式的,當我們用滑鼠複製 𝑥 字元到一個雙引號中時,idea 編輯器,會自動轉換為這樣的格式:

那麼,MySQL 的 utf8mb3 不支援哪些字元 ?

繼續看 MySQL官方文檔

可以看到,文檔中已經給出了比較明確的描述:

  • 僅支援 BMP 字元
  • 一個字元的編碼最多三個位元組。

到這裡,你可能又會問是什麼 BMP 字元嘞,Wiki 百科看不懂啊!

在介紹這個問題之前,首先要了解一點基礎知識 Code point

大家應該都認識這張表,ASCLL 包含 128 個 Code point 表示 128 個字元(也就是 0 ~ 127)。

在標準的 Unicode 中容納了 1,114,112 code points,其中前 65,536 個 Code point (也就是 0 ~ 65535)稱為 Basic Multilingual Plane(縮寫:BMP

  • 查看一個字元的 Code point 可以使用 charbase.com,示例,查看大寫字母 A :

  • 判斷一個字元是否是 BMP
    首先計算出字元的 Code point,然後檢查其範圍,如果在 0 ~ 65535 內,就是 BMP 字元。

2.2. MySQL UTF8 和 標準 UTF-8 編碼是一個概念嗎?

通過上一個問題,我們了解到,MySQL 5.7 版本中 UTF8utf8mb3 的別名,utf8mb3 是使用 1 ~ 3 個位元組對 Unicode 字元進行編碼,僅支援 BMP 字元。

在 Wiki 百科裡面對 UTF-8 的定義是:

簡言之:使用 1 ~ 4 個位元組對標準 Unicode 1,112,064 個有效的字元 Code point 進行編碼。

因此,這兩個 utf8 在不同的上下文背景下不是一個概念,很多開發人員包括我,經常在沒有對事物做詳細調研之前,憑藉主觀經驗對事物妄下結論。

三、程式語言最佳實踐

通過上面分析,我們知道問題的背景和原因。下面的給出最佳編程實踐,選取前/後端使用的兩門語言:

3.1. 在 Java 語言中檢測字元串中的非 BMP 字元

public class Main {

    public static void main(String[] args) {
        String str = "𝑥方程";
        boolean contain = isContainsNonBmpUnicodeCharacter(str);
        if (contain) {
            System.out.println("The string contains non-BMP Unicode character.");
        }
    }

    private static boolean isContainsNonBmpUnicodeCharacter(String str) {
        return str.length() != str.codePointCount(0, str.length());
    }
}

3.2.在 Javascript 中檢測字元串中非 BMP 字元

function main() {
    let str = "𝑥方程";
    let contains = isContainsNonBmpUnicodeCharacter(str);
    if (contains) {
        console.log("The string contains non-BMP Unicode character.");
    }
}

function isContainsNonBmpUnicodeCharacter(str) {
    return str.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g).length != 1;
}

參考文獻

  1. U+1D465: MATHEMATICAL ITALIC SMALL X – charbase.com

  2. How to convert a byte array to a hex string in Java? – stackoverflow.com

  3. Check if a String contains characters which aren’t UTF-8 encoded in Java – stackoverflow.com

  4. Unicode Character Sets

  5. The utf8mb3 Character Set (3-Byte UTF-8 Unicode Encoding)

  6. Basic Multilingual Plane – wikipedia.org

  7. Code point – wikipedia.org

  8. How do I convert unicode codepoints to their character representation?

  9. UTF-8 – wikipedia.org

  10. JavaScript strings outside of the BMP – stackoverflow.com