ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL utf8与utf8mb4区别详解:emoji存储、迁移步骤与踩坑指南

MySQL utf8与utf8mb4区别详解:emoji存储、迁移步骤与踩坑指南 如果你在业务表里存过 emoji大概率见过这条报错Incorrect string value: \xF0\x9F\x98\x80 for column nickname at row 1很多人的第一反应是“字符集没设对”然后去把表的字符集改成 utf8结果发现还是报错。真正的原因其实很简单MySQL 里的 utf8 并不是真正的 UTF-8它最多只支持 3 个字节而 这个 emoji 的 UTF-8 编码一共要 4 个字节。想要支持完整 Unicode得用utf8mb4。这篇东西就是专门聊utf8和utf8mb4的。我会把两者到底差在哪、为什么 MySQL 会搞出这么个历史包袱、从 utf8 迁到 utf8mb4 的完整步骤以及我调过几十个库之后踩过的坑一次说清楚。不管你是刚接触 MySQL 的开发者还是准备把老库字符集升级的运维都可以直接照着操作。1. 先搞清楚一个前提MySQL 里的 utf8 到底是什么1.1 名字叫 utf8本质是 utf8mb3MySQL 官方文档里其实写得很明确utf8这个字符集在 MySQL 里对应的是一种最多 3 字节的 UTF-8 实现官方叫法是utf8mb3。为什么会出现这种奇怪的事时间要拉回 MySQL 5.5.3 之前。早年间MySQL 4.0、5.0 时代Unicode 标准本身还没扩展到太多增补平面常用字符都在基本多文种平面BMPU0000 到 UFFFF范围内当时的 UTF-8 最多 3 字节就够覆盖。MySQL 就把这个“3 字节版 UTF-8”叫做utf8。但 Unicode 后来引入了增补平面Supplementary Plane范围扩展到 U10FFFF一个字符最多需要 4 字节来编码。emoji、部分 CJK 扩展 B 区生僻字、某些古文字符号都在这个范围里。MySQL 面临一个选择要么把utf8从 3 字节扩展成 4 字节要么新增一个字符集。当时 MySQL 选择了后者——保留utf8不动避免影响存量用户的行为同时从 5.5.3 开始引入utf8mb4mb 就是 most bytes最多 4 字节。于是 MySQL 里的utf8就成了一个“残缺的 UTF-8”。所以你今天看到的所有CHARSETutf8的库并不是真正的 UTF-8而是 utf8mb3。MySQL 8.0 的默认字符集已经切到了utf8mb4但 5.7 及更早版本如果你手动指定utf8它依然按 3 字节实现处理。1.2 “4 字节字符”具体指什么简单说凡是 Unicode 码点大于 UFFFF 的字符在 UTF-8 里都需要 4 个字节表示。常见的有emojiU1F600、U1F44D、U1F680CJK 扩展 B 区生僻字U20BB7、U2B820等部分数学符号、古文字、特殊排版符号这些字符在 3 字节的utf8utf8mb3里根本存不了。你往这类字段里插入 emoji轻则被截断成问号?重则直接报开头的Incorrect string value错误。我自己实际遇到过印象很深的一个案例业务方做一个用户昵称系统用户能正常注册但后台昵称里带 emoji 的全部变成??。一开始大家以为前端发过来就丢了后来抓包发现值是好端端四个字节 UTF-8问题就出在数据库列是CHARACTER SET utf8连接层也按 utf8 走服务端直接拒绝写入。一句话记住结论utf8mb4 才是完整支持 Unicode 的字符集utf8 只是个历史遗留的 3 字节子集。2. utf8 和 utf8mb4一字之差到底差在哪2.1 字符集能力对比先看一张结果清晰的对比表对比项utf8utf8mb3utf8mb4单字符最大字节数34支持 BMPU0000 ~ UFFFF是是支持增补平面emoji、生僻字否是存储英文字母/数字1 字节1 字节存储中文3 字节3 字节存储 emoji不支持4 字节默认排序规则8.0无utf8mb4_0900_ai_ci引入版本4.0 之前MySQL 5.5.38.0 默认很多人担心的“utf8mb4 会不会更浪费空间”实际测试下来几乎可以忽略。因为 UTF-8 是变长编码ASCII 字符在两个字符集下都是 1 字节中文字符都是 3 字节。只有遇到真正的 4 字节字符时才多 1 字节。业务数据里 4 字节字符的比例通常极低多出来的开销非常有限。真正的开销大头在索引和行大小我下面单独讲。2.2 存储和索引这笔账要算清楚MySQL 里VARCHAR(N)的N是字符数不是字节数。同样是VARCHAR(255)utf8 字符集下最多存 255 个中文字符占用 255×3 765 字节utf8mb4 字符集下也最多存 255 个中文字符占用同样是 765 字节但如果存满 4 字节字符占用可到 255×4 1020 字节行本身还好真正容易出问题的是索引长度限制。在老版本的 InnoDB 里5.6、5.7.7 之前单索引键最大只有 767 字节。这种情况下utf8 下varchar(255)建索引255×3 765 字节不超限utf8mb4 下varchar(191)建索引191×4 764 字节刚好能过一旦写成varchar(255)255×4 1020 字节直接报Specified key was too long; max key length is 767 bytesMySQL 5.7.7 之后默认开启innodb_large_prefix配合 DYNAMIC 行格式索引键上限提升到 3072 字节utf8mb4 下单列索引可以到 768 个字符。但很多老表迁移时行格式还是 COMPACTinnodb_large_prefix没开一样会被 767 卡死。所以我做字符集迁移前一定会先看表的行格式而不是闷头直接 ALTER。2.3 排序规则性能之外容易被忽略的点字符集改了排序规则collation往往也得跟着换。utf8 和 utf8mb4 各有自己的排序规则常见的有排序规则所属字符集特点utf8_general_ciutf8旧默认比较规则简单速度快utf8_unicode_ciutf8按 Unicode 规则排序准确但老版本性能稍低utf8mb4_general_ciutf8mb4utf8mb4 旧默认规则同上utf8mb4_unicode_ciutf8mb4通用选择比较准确utf8mb4_0900_ai_ciutf8mb4MySQL 8.0 默认基于 Unicode 9.0支持口音不敏感比较性能好这里有个容易踩的坑如果两张表连表 JOIN但它们的相关联字段 collation 不一致MySQL 会直接报Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation 所以迁移时尽量把整个库的字符集和排序规则统一不要库是utf8mb4_unicode_ci、表是utf8mb4_general_ci后面总会莫名其妙冒出一堆报错。关于中文排序多说一句不管general_ci还是unicode_ci对中文基本都是按码点排并不是拼音。业务上如果需要按拼音排序MySQL 8.0 可以用utf8mb4_zh_0900_as_cs或者直接在应用层做转换排序。3. 从 utf8 迁到 utf8mb4 的完整实操3.1 动手前先摸清现状别上来就 ALTER。先把库、表、列的当前字符集看一遍-- 查看服务器和连接层的字符集 SHOW VARIABLES LIKE character_set_%; SHOW VARIABLES LIKE collation_%; -- 查看指定库的默认字符集 SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME your_db; -- 查看每张表每列的字符集 SHOW FULL COLUMNS FROM your_table;SHOW FULL COLUMNS输出里会多一列Collation这列能直观看到哪些字段还是旧字符集。实际项目里经常会发现库是 utf8mb4但某些历史字段还是 utf8这种半吊子状态最容易出问题。查完之后顺手看一眼表行格式SELECT TABLE_NAME, ROW_FORMAT FROM information_schema.TABLES WHERE TABLE_SCHEMA your_db;如果是 COMPACT 或 REDUNDANT建议先评估索引长度问题免得 ALTER 到一半失败。3.2 修改库、表和字段的完整 SQL备份永远不能省。典型备份命令mysqldump --single-transaction --set-gtid-purgedOFF -u root -p your_db your_db_backup.sql注意备份文件里带着源库的字符集声明恢复的时候要看好连接字符集必要时命令行加--default-character-setutf8mb4。正式迁移分三步第一步改库默认字符集ALTER DATABASE your_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;第二步改表ALTER TABLE your_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;CONVERT TO会把表里所有字符类型的列一起转换。如果只想改某几列用 MODIFYALTER TABLE your_table MODIFY your_col VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;表特别多的时候可以直接用 information_schema 生成批量 SQLSELECT CONCAT(ALTER TABLE , table_name, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;) FROM information_schema.tables WHERE table_schema your_db;把查询结果复制出来确认一遍再执行别一把梭。第三步改服务器配置。编辑my.cnf[mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci [client] default-character-set utf8mb4然后重启 MySQL。如果是 MySQL 8.0[client]段可以省略但写上没坏处。这里有一个细节ALTER DATABASE和ALTER TABLE都是 DDL隐式提交事务不能回滚。所以大表操作一定要在维护窗口做先备份再执行。3.3 不只是数据库连接层的配置也要跟上很多人在这一步翻车。库表都改成 utf8mb4 了程序里连接还是按老字符集走结果写入 emoji 依旧变成?。Java JDBC 这块特别典型。正确写法是jdbc:mysql://localhost:3306/your_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai注意连接串里写UTF-8不要写utf8mb4。Java 标准字符集名没有utf8mb4老驱动会直接抛Unsupported character encoding utf8mb4。很多从 MySQL 5.x 时代过来的老项目踩过这个坑之后就对 utf8mb4 有心理阴影其实问题不在字符集本身而是连接参数写错了。Python 这边用 PyMySQL 的话是conn pymysql.connect( hostlocalhost, userroot, password..., databaseyour_db, charsetutf8mb4 )PHP 的 PDO 里则要同时设置 DSN 和 SET NAMES$pdo new PDO( mysql:hostlocalhost;dbnameyour_db;charsetutf8mb4, $user, $pass ); $pdo-exec(SET NAMES utf8mb4);如果你用 Navicat 这类图形客户端建连接时把“编码”选成utf8mb4老版本显示为 UTF-8 也可以不要在导入导出时默认走系统编码。判断连接层到底用的什么字符集最快的方式是SHOW VARIABLES LIKE character_set_client; SHOW VARIABLES LIKE character_set_connection;如果这两个值是 utf8说明客户端到服务端的连接还没切到 utf8mb4即使表结构是 utf8mb4也照样写不进 4 字节字符。4. 迁移过程中我踩过的坑4.1 索引超长导致 DDL 直接失败这是我第一次做全库字符集迁移时碰到的第一个大坑。老系统里好几张表在VARCHAR(255)字段上有唯一索引。utf8 时代 255×3 765 字节刚好压在 767 限制内一改成 utf8mb4255×4 1020 字节直接超限。解决办法看情况如果是 MySQL 5.7.7先确认行格式是 DYNAMIC并且innodb_large_prefix是 ON。如果老表是 COMPACT可以先改行格式ALTER TABLE your_table ROW_FORMATDYNAMIC;如果业务允许缩短索引列长度比如VARCHAR(191)。如果不是唯一索引可以改用前缀索引ALTER TABLE your_table ADD INDEX idx_name (your_col(191));这里我不建议为了让 DDL 顺利通过而直接砍字段长度一定要先跟业务确认查询对字段长度的实际需求。4.2 字段排序规则不一致引发的 JOIN 报错另一个坑出现在改造只做了一半的时候。库和大部分表改成了 utf8mb4但有几张老表漏掉了还是 utf8。业务 JOIN 时 MySQL 直接抛Illegal mix of collations错误。排查方法很简单SELECT TABLE_NAME, COLUMN_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA your_db AND COLLATION_NAME LIKE utf8\_% AND COLLATION_NAME NOT LIKE utf8mb4%;把所有遗留字段揪出来改掉。避免这个问题的最好习惯是一个库的字符集和 collation 全部统一不要出现库是 utf8mb4、某张表还是 utf8 的状态。分批迁移也要在当天全部收尾中间状态不要跨业务周期。4.3 连接层没改emoji 存进去还是问号有次同事很兴奋地告诉我“表已经改成 utf8mb4 了”但重新跑业务用户昵称里的 emoji 还是变成?。我去查数据库字符集确实已经生效问题出现在应用连接池的 JDBC 连接串里还写着characterEncodingutf8。这里额外提醒一点连接池如果之前已经连到过数据库修改连接参数后一定要重启应用或至少刷新连接池。Java 应用里很多连接池会复用旧连接你改了配置但不重建连接实际生效的还是老连接上的字符集。4.4 大表 ALTER 时间过长、锁表问题ALTER TABLE ... CONVERT TO CHARACTER SET会重写整张表和全部索引几百万行的表跑起来起码几分钟到几十分钟。期间表会被 metadata lock 锁住业务读写都可能受影响。对于大表我的建议是别在白天高峰期直接 ALTER选维护窗口。如果表实在太大而且允许用工具可以考虑pt-online-schema-change它通过触发器把变更在后台平滑执行避免长时间锁表。执行前先SET SESSION lock_wait_timeout 3600;避免拿不到锁直接超时失败。ALTER 是 DDL隐式提交事务没有回滚一说所以必须提前备份。还有个小技巧如果一张超大的表只需要改字符集不涉及其他字段变更可以先评估业务是否可以接受停机窗口。能接受的话直接 ALTER 最简单不能接受就用在线 DDL 工具。5. 经验总结与日常建议5.1 新项目直接用 utf8mb4别再纠结新库、新表、新连接一律utf8mb4。MySQL 8.0 的默认字符集就是 utf8mb4你什么都不设置新建的库也是它。5.7 的话建库时显式写CREATE DATABASE your_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;排序规则我一般这么选MySQL 8.0 用默认的utf8mb4_0900_ai_ci5.7 用utf8mb4_unicode_ci。除非有特殊排序需求不要随便换成general_ci。如果担心空间和性能先把数据跑起来再看。我真实的线上经验是utf8mb4 和 utf8 在绝大多数业务读写场景下性能差距可以忽略不计。真正决定性能的是索引设计、SQL 质量和数据量不是字符集这多出来的一两个字节。5.2 老项目要不要动先看数据再看风险老库升不升级没有标准答案但可以按下面几个维度评估业务上有没有存 emoji 或生僻字的需求有那就必须升。有没有开源系统要求数据库必须 utf8mb4比如 Zabbix、Nacos 等很多官方部署文档都硬性要求 utf8mb4否则初始化脚本就跑不过去。这种属于外部强约束直接升。库表特别大DBA 人手也不足业务也不涉及 4 字节字符。那可以暂时不动但要记录技术债别让后面接手的人继续往 utf8 库上堆数据。我的个人习惯是即使老库暂时不迁新建的表也一律 utf8mb4新增的连接参数也按 utf8mb4 写避免把坑越挖越深。最后再分享一个小技巧如果你只是想让某个会话临时支持 emoji不用改表也能先救急SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;这条语句会把 character_set_client、character_set_connection、character_set_results 三个值一起切到 utf8mb4。但注意它只对当前会话有效别指望它持久化。做了这么多年 MySQL 维护我见过太多因为字符集问题排查到半夜的案例。很多问题到最后发现就是建库时少写了一个mb4。MySQL 的utf8和utf8mb4之间的差别一句话就能讲完但牵扯出来的连接配置、索引限制、排序规则、迁移步骤每一环都值得认真对待。希望这篇总结能帮你少走点弯路。
返回列表