目录

UTF-8 编码与 MySQL 中的 utf8、utf8mb4

UTF-8编码

UTF-8 是一种变长字节编码方式。对于某一个字符的 UTF-8 编码,如果只有一个字节则其最高二进制位为 0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为 1 的个数决定了其编码的位数,其余各字节均以10开头。UTF-8 在设计上最多可用到6个字节。

如表:

字节数 UTF-8 编码
1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 中可以用来表示字符编码的实际位数最多有 31 位,即上表中 x 所表示的位。除去控制位(每字节开头的10等),这些 x 表示的位与 UNICODE 编码是一一对应的,位高低顺序也相同。

将 UNICODE 转换为 UTF-8 编码时应先去除高位 0,然后根据所剩编码的位数决定所需最小的 UTF-8 编码位数。

UNICODE 兼容 ASCII,只需要一个字节的 UTF-8 编码便可以表示。

示例

以“9月”为例,展示如何实现 UTF-8 编码。

  • UTF-8 编码:00111001——UNICODE:39——“9
  • UTF-8 编码:11100110 10011100 10001000——UNICODE:E6 9C 88——“

UNICODE 与 UTF-8 转换

Unicode 符号范围(十六进制) UTF-8 编码方式(二进制)
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

汉字“严”为例,已知“严”的 Unicode是 4E25(100 111000 100101),根据上表,4E25 处在第三行的范围内(07FF-FFFF),因此“严”的 UTF-8 编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。因此“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是 E4B8A5。

MySQL 中的 utf8

MySQL 从 4.1 版本开始支持 utf8 编码,而标准的 UTF-8 编码在其之后才正式发布。准确地说,MySQL 中 utf8 的全名应该叫——utf8mb3(max byte 3),其编码的最大字符长度为 3 字节,而三个字节的 UTF-8 最大能编码的 Unicode 字符是 0xFFFF,也就是 Unicode 中的基本多文平面(BMP)。也就是说,任何不在基本多文平面的 Unicode字符,都无法使用MySQL原有的 utf8 字符集存储。

中文基本汉字+基本汉字补充的 Unicode 编码范围为:4E00-9FCB,共计收录 20940个汉字,而扩展 B 编码范围为 20000-2A6D6,扩展 C 为2A700-2B734,扩展 D为 2B740-2B81D,共计 47082 个汉字,以及其他的兼容扩展等。这些编码范围大于 FFFF 的不常用汉字、我们常见的 Emoji 表情(Emoji 是一种特殊的 UNICODE 编码)、任何新增的 Unicode 字符等等,在 MySQL 中并不会完整地存储下来,并导致了整个字段的乱码。

于是在 5.5.3 版本中,MySQL 添加了对 utf8mb4 编码的支持,最大字符长度为四字节,也就是说 utf8mb4 才是我们传统意义上的 UTF-8。然而遗憾的是,在 MySQL 的众多分支版本中,只有 8.0.x 分支中默认开启的是 utfmb4,其他分支默认依然是 utf8 编码。

所以,永远不要在MySQL中使用 utf8,改用 utf8mb4。

MySQL 简史

为什么 MySQL 开发者会让“utf8”失效?我们或许可以从提交日志中寻找答案。

MySQL 从4.1版本开始支持 UTF-8,也就是2003年,而今天使用的 UTF-8 标准(RFC 3629)是随后才出现的。

旧版的 UTF-8 标准(RFC 2279)最多支持每个字符6个字节。2002年3月28日,MySQL开发者在第一个MySQL 4.1预览版中使用了RFC 2279。

同年9月,他们对 MySQL 源代码进行了一次调整:『UTF8 现在最多只支持 3 个字节的序列』。

是谁提交了这些代码?他为什么要这样做?这个问题不得而知。在迁移到 Git 后(MySQL最开始使用的是 BitKeeper),MySQL 代码库中的很多提交者的名字都丢失了。2003 年 9 月的邮件列表中也找不到可以解释这一变更的线索。

不过我可以试着猜测一下。

2002 年,MySQL 做出了一个决定:如果用户可以保证数据表的每一行都使用相同的字节数,那么 MySQL 就可以在性能方面来一个大提升。为此,用户需要将文本列定义为CHAR,每个CHAR列总是拥有相同数量的字符。如果插入的字符少于定义的数量,MySQL 就会在后面填充空格,如果插入的字符超过了定义的数量,后面超出部分会被截断。

MySQL 开发者在最开始尝试 UTF-8 时使用了每个字符 6 个字节,CHAR(1) 使用 6 个字节,CHAR(2) 使用 12 个字节,并以此类推。

应该说,他们最初的行为才是正确的,可惜这一版本一直没有发布。但是文档上却这么写了,而且广为流传,所有了解 UTF-8 的人都认同文档里写的东西。

不过很显然,MySQL 开发者或厂商担心会有用户做这两件事:

  1. 使用 CHAR 定义列(在那时,在 MySQL 中使用 CHAR 会更快,不过从 2005 年以后就不是这样子了);
  2. 将 CHAR 列的编码设置为utf8

我们猜测是 MySQL 开发者本来想帮助那些希望在空间和速度上双赢的用户,但他们搞砸了 utf8 编码。

所以结果就是没有赢家。那些希望在空间和速度上双赢的用户,当他们在使用utf8的 CHAR 列时,实际上使用的空间比预期的更大,速度也比预期的慢。而想要正确性的用户,当他们使用“utf8”编码时,却无法保存像『😀』这样的字符。

在这个不合法的字符集发布了之后,MySQL 就无法修复它,因为这样需要要求所有用户重新构建他们的数据库。最终,MySQL 在 2010 年重新发了utf8mb4来支持真正的 UTF-8。