社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

永远不要在 MySQL 中使用「utf8」

MacTalk • 6 年前 • 701 次点击  

题图:by apple from Instagram

本文出自极客邦「聊聊架构」公众号的编译。我自己当年也被这个问题坑过,当时并没有如此详细的分析文章。我觉得有责任再次分享一下,让更多人知道这个事情。程序员不帮助程序员,还能指望谁呢?


最近我遇到了一个 bug,我试着通过 Rails 在以「utf8」编码的 MariaDB 中保存一个 UTF-8 字符串,然后出现了一个离奇的错误:

Incorrect string value: ‘\xF0\x9F\x98\x83

我用的是 UTF-8 编码的客户端,服务器也是 UTF-8 编码的,数据库也是,就连要保存的这个字符串「

问题的症结在于,MySQL 的「utf8」实际上不是真正的 UTF-8。

「utf8」只支持每个字符最多三个字节,而真正的 UTF-8 是每个字符最多四个字节。MySQL 一直没有修复这个 bug,他们在 2010 年发布了一个叫作「utf8mb4」的字符集,绕过了这个问题。当然,他们并没有对新的字符集广而告之(可能是因为这个 bug 让他们觉得很尴尬),以致于现在网络上仍然在建议开发者使用「utf8」,但这些建议都是错误的。

简单概括如下:

1、MySQL 的「utf8mb4」是真正的「UTF-8」。
2、MySQL 的「utf8」是一种「专属的编码」,它能够编码的 Unicode 字符并不多。

我要在这里澄清一下:所有在使用「utf8」的 MySQL 和 MariaDB 用户都应该改用「utf8mb4」,永远都不要再使用「utf8」。

什么是编码?什么是 UTF-8?

我们都知道,计算机使用 0 和 1 来存储文本。比如字符「C」被存成「01000011」,那么计算机在显示这个字符时需要经过两个步骤:

1、计算机读取「01000011」,得到数字 67,因为 67 被编码成「01000011」。
2、计算机在 Unicode 字符集中查找 67,找到了「C」。

同样的:

1、我的电脑将「C」映射成 Unicode 字符集中的 67。
2、我的电脑将 67 编码成「01000011」,并发送给 Web 服务器。

几乎所有的网络应用都使用了 Unicode 字符集,因为没有理由使用其他字符集。

Unicode 字符集包含了上百万个字符。最简单的编码是 UTF-32,每个字符使用 32 位。这样做最简单,因为一直以来,计算机将 32 位视为数字,而计算机最在行的就是处理数字。但问题是,这样太浪费空间了。

UTF-8 可以节省空间,在 UTF-8 中,字符「C」只需要 8 位,一些不常用的字符,比如「」需要 32 位。其他的字符可能使用 16 位或 24 位。一篇类似本文这样的文章,如果使用 UTF-8 编码,占用的空间只有 UTF-32 的四分之一左右。

MySQL 的「utf8」字符集与其他程序不兼容,它所谓的字符集,可能真的是一坨……

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 定义列(在现在看来,CHAR 已经是老古董了,但在那时,在 MySQL 中使用 CHAR 会更快,不过从 2005 年以后就不是这样子了)。
2 将 CHAR 列的编码设置为「utf8」。

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

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

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

为什么这件事情会让人如此抓狂

因为这个问题,我整整抓狂了一个礼拜。我被「utf8」愚弄了,花了很多时间才找到这个 bug。但我一定不是唯一的一个,网络上几乎所有的文章都把「utf8」当成是真正的 UTF-8。「utf8」只能算是个专有的字符集,它给我们带来了新问题,却一直没有得到解决。

写在最后

如果你在使用 MySQL 或 MariaDB,不要用「utf8」编码,改用「utf8mb4」。这里提供了一个指南用于将现有数据库的字符编码从「utf8」转成「utf8mb4」:

https://mathiasbynens.be/notes/mysql-utf8mb4#utf8-to-utf8mb4

这篇文章的英文原文:
https://medium.com/@adamhooper/in-mysql-never-use-utf8-use-utf8mb4-11761243e434


卖桃者说:

微信订阅号改为信息流的浏览方式之后,新的文章很难对旧文实现二次阅读唤醒功能,那我自己来吧,近期推荐阅读:

新 Mac 和 iOS 12
为什么你不愿下载 App 了
学点 JVM
别谈初心,一谈我就躺下了


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/21jJ5UL2MT
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/21210
 
701 次点击