MySQL字符集陷阱:从Oracle迁移踩坑到utf8mb4强制规范

MySQL字符集陷阱:从Oracle迁移踩坑到utf8mb4强制规范 一、踩坑现场一次跨库迁移的翻车实录1.1 背景去年我做Oracle → mysql数据库迁移的时候源库 Oracle 里有一张表sql-- 源端 OracleCREATE TABLE customer_blacklist ( cust_name VARCHAR2(200), -- 客户姓名200 字节 reason VARCHAR2(500), -- 屏蔽原因 operator VARCHAR2(50));字段看起来平平无奇对吧问题就出在客户姓名这四个字上。1.2 出问题的那个客户某天运营反馈有个维吾尔族客户的姓名存不进去报错value too large。我一看报错就懵了 —— 200 字节怎么可能不够一个姓名打开数据一看客户姓名: 买买提·阿不都热依木·买买提敏看起来不长对吧问题在于热依木这种带波浪号~ 字符的少数民族姓名用了 2 个码点表示一个字符加上 UTF-8 编码 3 字节一个码点text买买提·阿不都热依木·买买提敏 14 个字符 × 最多 3 字节 42 字节这不是 200 字节够不够的问题是字符集选错的问题。1.3 根因诊断我翻代码发现这个项目早期就埋了雷sql-- 早年代码迁移前 Oracle 端CREATE TABLE customer_blacklist ( cust_name VARCHAR2(200 CHAR) -- 注意是 CHAR 不是 BYTE);VARCHAR2 加了CHAR关键字后N 表示字符数200 字符这在 Oracle 里是合规写法。但后来业务发展新加了一张表做扩展sql-- 后加的表迁移前 Oracle 端工程师偷懒没加 CHARCREATE TABLE customer_ext ( cust_name VARCHAR2(200) -- 默认是 BYTE200 字节);结果就是在 Oracle 里写维吾尔族姓名 标点 → 存得下但字符集一旦变成 UTF-8 编码GBK 也罢UTF-8 也罢一个字符最多 4 字节emoji200 字节最多存 50 个字符。这个雷在我接手前就埋了直到做 GaussDB 迁移时数据导入才炸出来。二、utf8 vs utf8mb4MySQL 的骗子字符集2.1 一个反常识的事实我先问你一个问题MySQL 的 utf8 字符集是完整的 UTF-8 编码吗99% 的 Java 开发会回答是。答案是不是。MySQL 的utf8字符集只支持最长 3 字节的 UTF-8 编码字符不完整。这是 MySQL 历史上的一个设计失误为了兼容老的 utf8 编码最大 3 字节保留了下来。完整的 UTF-8 编码字符集在 MySQL 里叫utf8mb4—— mb 是 max byte 的缩写4 表示支持 4 字节。2.2 直接对比字符集最大字节数实际意义适用场景MySQL utf83 字节阉割版 UTF-8不支持 emoji / 4 字节字符❌ 已过时别用MySQL utf8mb44 字节真正完整的 UTF-8✅MySQL 8.0 默认强制使用gbk2 字节早期中文编码❌ 不支持国际化已过时latin11 字节西欧字符MySQL 早期默认2.3 一句口诀MySQL 的 utf8 是骗子utf8mb4 才是真 utf8这是我在公司 Code Review 里强制每个新工程师背的口诀。原因下面会说。2.4 哪些字符会装不下Emoji 表情 4 字节生僻字龘 三个龙字 4 字节部分少数民族文字如上文热依木的波浪号组合部分数学符号如 4 字节CJK 扩展区汉字如䶮所有这些字符在 MySQL utf8 下都存不进去。要么报错要么变成问号?。三、金融项目为什么要强制 utf8mb4我用一句话总结金融项目如果不上 utf8mb4迟早会因为字符问题在生产环境翻车。为什么我给你列 5 个真实的金融业务场景场景 1客户姓名sql-- 客户表CREATE TABLE customer ( name VARCHAR(100) -- 100 字符utf8mb4 下最大 400 字节);客户类型字符数字节数utf8mb4存得下吗普通中文名张三26✅维吾尔族长名14最多 56✅客户昵称投资达人7含 emoji13✅维吾尔族姓名utf8 字符集1414 × 3 42OK⚠️维吾尔族姓名utf8 emoji1414 × 3 4 46❌ 报错场景 2跨境业务繁体 / 日韩sqlCOMMENT 客戸姓名 -- 繁体COMMENT 성함 -- 韩文繁体字 3 字节 / 字utf8 / gbk 都是 2 字节utf8mb4 也是 3 字节够用韩文 3 字节 / 字日文含汉字假名3 字节 / 字utf8 字符集对这些也够用3 字节但加上 emoji 就不行了。场景 3员工昵称 / 用户名sql-- 越来越多人用 emoji 当昵称INSERT INTO user (name) VALUES (老司机); 是 4 字节 emoji 3 个中文字符 4 9 13 字节。utf8 字符集存不下utf8mb4 才行。场景 4交易备注sqlINSERT INTO trade_remark (content) VALUES (打款成功已通知客户);4 字节 emoji 中文字符只有 utf8mb4 存得下。场景 5合规日志监管报送sql-- 反洗钱系统风险标记INSERT INTO aml_log (warning) VALUES (⚠️ 客户身份证件 OCR 识别异常);合规日志要求完整保留原始信息任何字符截断都是合规风险。公司规约Alibaba Java 开发手册、字节跳动、蚂蚁金服的 MySQL 规约都强制要求 utf8mb4。这不是个人偏好是行业共识。四、MySQL 5.7 vs 8.0 的默认字符集变迁很多老项目还在用 MySQL 5.7新项目用 8.0默认值不一样MySQL 版本默认字符集说明5.6latin1西方字符完全不推荐5.7latin1同上但官方建议改为 utf8mb48.0utf8mb4官方默认值强制使用金融项目新库标准模板Alibaba 规约版sqlCREATE DATABASE xxxDEFAULT CHARACTER SET utf8mb4COLLATE utf8mb4_unicode_ci;注意两个细节utf8mb4_unicode_ci基于 Unicode 标准的排序规则比utf8mb4_general_ci准确但稍慢utf8mb4_general_ci更快但排序结果不准确金融报表排序不推荐五、字节 vs 字符 vs 码点3 个概念别再混了这块有个 99% 的 Java 开发都会搞错的点MySQL 的 VARCHAR(N) 的 N 是字符数不是字节数。5.1 三个概念分清概念含义例子字符人能识别的一个文字A、中、码点字符在 Unicode 表里的编号A U0041中 U4E2D U1F600字节存储/传输的最小单位物理存储字符 ↔ 码点 1:1 对应Unicode 内字节数 编码方式决定。5.2 编码方式决定 1 字符 几字节编码1 字符 几字节中 占几字节ASCII1 字节固定N/A不含中文UTF-81-4 字节变长3 字节UTF-162 或 4 字节2 字节UTF-324 字节固定4 字节GBK1-2 字节2 字节5.3 MySQLVARCHAR(N)的 N 是字符数sqlCREATE TABLE t (name VARCHAR(100)) DEFAULT CHARSETutf8mb4;-- VARCHAR(100) 最多 100 个字符-- 实际存储字节数 字符数 × 单字符字节数-- BMP 字符 中 100 字符 × 3 字节 300 字节-- 4 字节字符 100 字符 × 4 字节 400 字节字符集VARCHAR(100) 最大字节utf8100 × 3 300 字节utf8mb4100 × 4 400 字节5.4 MySQL 两个长度函数sqlSELECT CHAR_LENGTH(中) AS 字符数, -- 1 LENGTH(中) AS 字节数; -- 3函数测的是CHAR_LENGTH(str)字符数语义层LENGTH(str)字节数物理层5.5 一个 Java 开发特别容易踩的坑java// ❌ 大错特错String name 买买提·阿不都热依木;int len name.length(); // 14Java String 的 length 是码点数if (len 50) { // 限制字符数}java// ✅ 正确// 用 BreakIterator / Code Point 来计算用户感知的字符数int graphemeCount name.codePointCount(0, name.length());Java 的String.length()返回的是char 数组的长度UTF-16 code unit不是字符数输入Java String.length()实际字符数原因中11BMP 字符1 个 char214 字节字符 2 个 charsurrogate pair买买提·阿不都热依木1414BMP 字符全是 1 个 char六、行总长 65535 字节限制另一个隐藏炸弹sql-- ❌ 报错Row size too large ( 65535)CREATE TABLE t ( a VARCHAR(22000), -- utf8mb4 下 88000 字节 ❌ b VARCHAR(16383) -- utf8mb4 下 65532 字节 ✓);MySQL 单行总长最大 65535 字节不含 TEXT/BLOB。算上 utf8mb4 的 4 字节 / 字符字符集VARCHAR(N) 中 N 的最大理论值latin165535utf82184565535/3utf8mb41638365535/4所以 utf8mb4 下 VARCHAR 最多 16383 字符。Oracle 的 VARCHAR2(4000 字节) 迁到 MySQL utf8mb4 时要注意4000/4 1000 字符不是 4000 字符七、utf8mb4 命名约定为什么叫 mb4 不叫 utf8_47.1 命名解析utf8mb4这个命名看着很奇怪为什么不直接叫utf8_4或utf8_v2MySQL 官方说法是mb max byte4 4 字节bytes 复数。也就是说utf8mb4 是个约定俗成的命名意思是最大支持 4 字节的 UTF-8 编码。7.2 类似的命名对比字符集命名逻辑含义utf8UTF-8阉割版3 字节utf8mb4UTF-8 max byte 4完整版4 字节utf8mb3UTF-8 max byte 3 utf8MySQL 8.0 新增别名utf16UTF-1616 位变长utf32UTF-3232 位定长MySQL 8.0 之后官方推荐用utf8mb4而不是utf8更清晰。八、动手验证你的 MySQL 默认字符集对吗最后做个动手验证看看你的 MySQL 是不是 utf8mb4sql-- 查看默认字符集SHOW VARIABLES LIKE character_set_server;SHOW VARIABLES LIKE collation_server;-- 查看已有库字符集SELECT default_character_set_name, default_collation_nameFROM information_schema.SCHEMATAWHERE schema_name 你的库名;-- 查看已有表字符集SELECT table_name, table_collationFROM information_schema.TABLESWHERE table_schema 你的库名LIMIT 5;如果你的character_set_server不是utf8mb4sql-- 修改配置文件 my.cnf / my.ini[mysqld]character-set-server utf8mb4collation-server utf8mb4_unicode_ci[client]default-character-set utf8mb4修改后重启 MySQL。但要注意已存在的库和表要单独改MySQL 不会自动改sqlALTER DATABASE your_dbCHARACTER SET utf8mb4COLLATE utf8mb4_unicode_ci;ALTER TABLE your_tableCONVERT TO CHARACTER SET utf8mb4COLLATE utf8mb4_unicode_ci;九、结语MySQL 的 utf8 是骗子utf8mb4 才是真 utf8金融项目 Oracle → MySQL先扫源端 VSIZE 找最大字节占用再除 4 算字符数VARCHAR(N) 的 N 是字符数不是字节数4 字节字符 emoji 4 字节行总长 65535 字节utf8mb4 下 VARCHAR 最多 16383 字符