Contents
  1. 1. 标准UTF-8字符集
  2. 2. MySQL的utf8
  3. 3. utf8_unicode_ci和utf8_general_ci区别
  4. 4. 怎么从utf8转换为utf8mb4
    1. 4.1. “伪”转换
    2. 4.2. character-set-server
  5. 5. key 767 bytes 错误
  6. 6. C/C++ 内存空间分配问题
  7. 7. java驱动使用
  8. 8. 主从复制报错
  9. 9. join 查询问题

标准UTF-8字符集

UTF-8(8-bit Unicode Transformation Format)即8位Unicode转换格式,是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字优先采用的编码。

UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):

  1. 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
  2. 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。
  3. 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。
  4. 其他极少使用的Unicode 辅助平面的字符使用四至六字节编码(Unicode范围由U+10000至U+1FFFFF使用四字节,Unicode范围由U+200000至U+3FFFFFF使用五字节,Unicode范围由U+4000000至U+7FFFFFFF使用六字节)。
    对上述提及的第四种字符而言,UTF-8使用四至六个字节来编码似乎太耗费资源了。但UTF-8对所有常用的字符都可以用三个字节表示,而且它的另一种选择,UTF-16编码,对前述的第四种字符同样需要四个字节来编码,所以要决定UTF-8或UTF-16哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用Unicode标准压缩格式(SCSU)。

互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。

MySQL的utf8

官方手册 https://dev.mysql.com/doc/refman/5.6/en/charset-unicode-sets.html

utf8mb4: A UTF-8 encoding of the Unicode character set using one to four bytes per character.

utf8mb3: A UTF-8 encoding of the Unicode character set using one to three bytes per character.

utf8: An alias for utf8mb3.

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

utf8mb3 supports only characters in the Basic Multilingual Plane (BMP). utf8mb4 additionally supports supplementary characters that lie outside the BMP.

utf8mb3 uses a maximum of three bytes per character. utf8mb4 uses a maximum of four bytes per character.

从手册中可以看到,utf8实际上是utf8mb3的别名,即maximum of three bytes per character最大只支持3个字节,MySQL官方并未直接修复这个问题,而是在5.5.3之后增加了utf8mb4字符编码,mb4即 maximum of four bytes。简单说 utf8mb4 是 utf8 的超集并完全兼容utf8,能够用四个字节存储更多的字符。

但抛开数据库,标准的 UTF-8 字符集编码是可以用 1~4 个字节去编码21位字符,这几乎包含了是世界上所有能看见的语言了。然而在MySQL里实现的utf8最长使用3个字节,也就是上面提到的标准UTF-8中“基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。”但并不是所有,最常见的就算现在手机端常用的表情字符emoji,需要四个字节(Unicode范围由U+10000至U+1FFFFF)才能编码出来。

注:QQ里面的内置的表情不算,它是通过特殊映射到的一个gif图片。一般输入法自带的才是emoji。

也就是当你的数据库里要求能够存入这些表情或宽字符时,可以把字段定义为 utf8mb4,同时要注意连接字符集也要设置为utf8mb4,否则在 严格模式 下会出现 Incorrect string value: \xF0\x9F\x98\x84… for column ‘content’这样的错误,非严格模式下此后的数据会被截断。

关于网上流传的latin1也支持emoji,Does mysql latin1 also support emoji character?
这里有一个问题就是,输入源要支持标准的UTF-8,操作系统的字符集为utf8(LANG=en_US.UTF-8)目前大部分系统都支持, 而client、connection、database均为latin1, 于是这一路(从终端界面执行insert到保存数据到表中)都没有编码转换,直接传输的是utf8编码后的二进制流。
latin1字符集的表,用户写入和读取汉字、emoji是否有问题?答案是只要设置合理,是没有问题的。假设SecureCRT为UTF8,character_set_client和表字符集均设置为latin1,那么用户读取和写入数据的过程中,并不涉及字符集编码转换的过程,将UTF8的汉字或emoji字符转为二进制流写入database,提取出来后,secureCRT再将对应的二进制解码为对应的汉字或emoji,所以不影响用户的使用。


utf8_unicode_ci和utf8_general_ci区别

字符除了需要存储,还需要排序或比较大小,涉及到与编码字符集对应的排序字符集(collation)。ut8mb4对应的排序字符集常用的有 utf8mb4_unicode_ci、utf8mb4_general_ci,到底采用哪个在 stackoverflow 上有个讨论,What’s the difference between utf8_general_ci and utf8_unicode_ci

在数据库系统MySQL或MariaDB中有多种字符集,其中utf8_unicode_ci和utf8_general_ci是最常用的,但是utf8_general_ci对某些语言的支持有一些小问题,如果可以接受,那最好使用utf8_general_ci,因为它速度快。否则,请使用较为精确的utf8_unicode_ci,不过速度会慢一些。

怎么从utf8转换为utf8mb4

“伪”转换

如果你的表定义和连接字符集都是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。所以我才叫它“伪”转换

character-set-server

一旦你决定使用utf8mb4,强烈建议你要修改服务端 character-set-server=utf8mb4,不同的语言对它的处理方法不一样,c++, php, python可以设置character-set,但java驱动依赖于 character-set-server 选项,后面有介绍。

同时还要谨慎一些特殊选项,个人不建议设置全局 init_connect。

key 767 bytes 错误

字符集从utf8转到utf8mb4之后,最容易引起的就是索引键超长的问题。

对于表行格式是 COMPACT或 REDUNDANT,InnoDB有单个索引最大字节数 768 的限制,而字段定义的是能存储的字符数,比如 VARCHAR(200) 代表能够存200个汉字,索引定义是字符集类型最大长度算的,即 utf8 maxbytes=3, utf8mb4 maxbytes=4,算下来utf8和utf8mb4两种情况的索引长度分别为600 bytes和800bytes,后者超过了767,导致出错: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 膨胀厉害的一个原因。

C/C++ 内存空间分配问题

这是我们这边的开发遇到的一个棘手的问题。C或C++连接MySQL使用的是linux系统上的 libmysqlclient 动态库,程序获取到数据之后根据自定义的一个网络协议,按照mysql字段定义的固定字节数来传输数据。从utf8转utf8mb4之后,c++里面针对character单字符内存空间分配,从3个增加到4个,引起异常。

这个问题其实是想说明,使用utf8mb4之后,官方建议尽量用 varchar 代替 char,这样可以减少固定存储空间浪费(关于char与varchar的选择,可参考 这里)。但开发设计表时 varchar 的大小不能随意加大,它虽然是变长的,但客户端在定义变量来获取数据时,是以定义的为准,而非实际长度。按需分配,避免程序使用过多的内存。

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。然而其他语言没有依赖于这样的特性。

主从复制报错

这个问题没有遇到,只是看官方文档有提到,曾经也看到过类似的技术文章。
大概就是从库的版本比主库的版本低,导致有些字符集不支持;或者人工修改了从库上的表或字段的字符集定义,都有可能引起异常。

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的隐式类型转换,见这里)。

参考
Mysql latin1也支持emoji字符的错觉分析
Mysql字符集知识总结
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乱码问题

Contents
  1. 1. 标准UTF-8字符集
  2. 2. MySQL的utf8
  3. 3. utf8_unicode_ci和utf8_general_ci区别
  4. 4. 怎么从utf8转换为utf8mb4
    1. 4.1. “伪”转换
    2. 4.2. character-set-server
  5. 5. key 767 bytes 错误
  6. 6. C/C++ 内存空间分配问题
  7. 7. java驱动使用
  8. 8. 主从复制报错
  9. 9. join 查询问题