一、踩坑现场:一次跨库迁移的翻车实录
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 端,工程师偷懒没加 CHAR)CREATE TABLE customer_ext ( cust_name VARCHAR2(200) -- 默认是 BYTE,200 字节);
结果就是 :在 Oracle 里写"维吾尔族姓名 + 标点" → 存得下,但字符集一旦变成 UTF-8 编码 (GBK 也罢,UTF-8 也罢),一个字符最多 4 字节 (emoji),200 字节最多存 50 个字符。
这个雷在我接手前就埋了,直到做 GaussDB 迁移时数据导入才炸出来。
二、utf8 vs utf8mb4:MySQL 的"骗子"字符集
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 "utf8" | 3 字节 | 阉割版 UTF-8,不支持 emoji / 4 字节字符 | ❌ 已过时,别用 |
| MySQL "utf8mb4" | 4 字节 | 真正完整的 UTF-8 | ✅ MySQL 8.0 默认,强制使用 |
| gbk | 2 字节 | 早期中文编码 | ❌ 不支持国际化,已过时 |
| latin1 | 1 字节 | 西欧字符 | 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) | 存得下吗 |
|---|---|---|---|
| 普通中文名"张三" | 2 | 6 | ✅ |
| 维吾尔族长名 | 14 | 最多 56 | ✅ |
| 客户昵称"😀投资达人" | 7(含 emoji) | 13 | ✅ |
| 维吾尔族姓名(utf8 字符集) | 14 | 14 × 3 = 42(OK) | ⚠️ |
| 维吾尔族姓名(utf8 + emoji) | 14 | 14 × 3 + 4 = 46 | ❌ 报错 |
场景 2:跨境业务(繁体 / 日韩)
sql
COMMENT '客戸姓名' -- 繁体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:交易备注
sql
INSERT 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.6 | latin1 |
西方字符,完全不推荐 |
| 5.7 | latin1 |
同上,但官方建议改为 utf8mb4 |
| 8.0 | utf8mb4 |
官方默认值,强制使用 |
金融项目新库标准模板(Alibaba 规约版):
sql
CREATE 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' = U+0041,'中' = U+4E2D,'😀' = U+1F600 |
| 字节 | 存储/传输的最小单位 | 物理存储 |
字符 ↔ 码点 1:1 对应 (Unicode 内),字节数 = 编码方式决定。
5.2 编码方式决定 1 字符 = 几字节
| 编码 | 1 字符 = 几字节 | "中" 占几字节 |
|---|---|---|
| ASCII | 1 字节固定 | N/A(不含中文) |
| UTF-8 | 1-4 字节变长 | 3 字节 |
| UTF-16 | 2 或 4 字节 | 2 字节 |
| UTF-32 | 4 字节固定 | 4 字节 |
| GBK | 1-2 字节 | 2 字节 |
5.3 MySQL VARCHAR(N) 的 N 是字符数
sql
CREATE TABLE t (name VARCHAR(100)) DEFAULT CHARSET=utf8mb4;-- VARCHAR(100) 最多 100 个字符-- 实际存储字节数 = 字符数 × 单字符字节数-- BMP 字符 "中" = 100 字符 × 3 字节 = 300 字节-- 4 字节字符 "😀" = 100 字符 × 4 字节 = 400 字节
| 字符集 | VARCHAR(100) 最大字节 |
|---|---|
| utf8 | 100 × 3 = 300 字节 |
| utf8mb4 | 100 × 4 = 400 字节 |
5.4 MySQL 两个长度函数
sql
SELECT CHAR_LENGTH('中') AS 字符数, -- 1 LENGTH('中') AS 字节数; -- 3
| 函数 | 测的是 |
|---|---|
CHAR_LENGTH(str) |
字符数(语义层) |
LENGTH(str) |
字节数(物理层) |
5.5 一个 Java 开发特别容易踩的坑
java
// ❌ 大错特错String name = "买买提·阿不都热依木";int len = name.length(); // 14(Java 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() | 实际字符数 | 原因 |
|---|---|---|---|
| "中" | 1 | 1 | BMP 字符,1 个 char |
| "😀" | 2 | 1 | 4 字节字符 = 2 个 char(surrogate pair) |
| "买买提·阿不都热依木" | 14 | 14 | BMP 字符,全是 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 的最大理论值 |
|---|---|
| latin1 | 65535 |
| utf8 | 21845(65535/3) |
| utf8mb4 | 16383(65535/4) |
所以 utf8mb4 下 VARCHAR 最多 16383 字符 。Oracle 的 VARCHAR2(4000 字节) 迁到 MySQL utf8mb4 时要注意:4000/4 = 1000 字符(不是 4000 字符!)
七、utf8mb4 命名约定:为什么叫 mb4 不叫 utf8_4
7.1 命名解析
utf8mb4 这个命名看着很奇怪,为什么不直接叫 utf8_4 或 utf8_v2?
MySQL 官方说法是:"mb" = max byte,"4" = 4 字节,"bytes" 复数。
也就是说:utf8mb4 是个约定俗成的命名,意思是"最大支持 4 字节的 UTF-8 编码"。
7.2 类似的命名对比
| 字符集 | 命名逻辑 | 含义 |
|---|---|---|
utf8 |
UTF-8 | 阉割版,3 字节 |
utf8mb4 |
UTF-8 + max byte 4 | 完整版,4 字节 |
utf8mb3 |
UTF-8 + max byte 3 | = utf8(MySQL 8.0 新增别名) |
utf16 |
UTF-16 | 16 位变长 |
utf32 |
UTF-32 | 32 位定长 |
MySQL 8.0 之后官方推荐用 utf8mb4 而不是 utf8,更清晰。
八、动手验证:你的 MySQL 默认字符集对吗?
最后做个动手验证,看看你的 MySQL 是不是 utf8mb4:
sql
-- 查看默认字符集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 不是 utf8mb4:
sql
-- 修改配置文件 my.cnf / my.ini[mysqld]character-set-server = utf8mb4collation-server = utf8mb4_unicode_ci[client]default-character-set = utf8mb4
修改后重启 MySQL 。但要注意:已存在的库和表要单独改(MySQL 不会自动改):
sql
ALTER 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 字符"