摘要:MySQL 的
utf8是 UTF-8 的残缺实现,最多只存 3 字节,而 Emoji 是 4 字节——只要表级没改成utf8mb4,写入就会报Incorrect string value。Mall4j 电商系统里,含 Emoji 的输入让搜索和系统参数两个互不相干的接口同时返回通用异常码 A00005。我通过换接口验证、翻后端堆栈、逐层查字符集,最终锁定根因是建表脚本遗留的CHARSET=utf8mb3。修复只需一行 SQL,但排查路径比 bug 本身更值得记。
前言:这篇不是教程,是一次完整排查的还原。很多人踩这个坑可能是以为配了 utf8mb4 就万事大吉,实际上表级和列级可能还停留在老字符集上。
一、一条边界值用例挂了
上周我在给 Mall4j 补搜索接口的边界值测试,矩阵里有这么一组:
@pytest.mark.parametrize("keyword, desc", [
("A" * 500, "超长文本"),
(" test ", "前后带空格"),
("admin' OR '1'='1", "SQL 注入"),
("<script>alert(1)</script>", "XSS 脚本"),
("😀😂🤔", "Emoji 表情"),
("!@#$%^&*()", "特殊字符"),
])
def test_search_with_invalid_keyword(buyer_headers, keyword, desc):
product = BuyerProduct(buyer_headers)
result = product.search_prod(prod_name=keyword)
assert result["code"] != "A00005", f"[{desc}] 返回服务器异常"
5 条全绿,唯独 Emoji 那条返回了服务器异常。
图1: 5 条边界值用例,唯一 Emoji 那条挂了
A00005 是 Mall4j 的通用异常码,由全局异常处理器兜底,看不出任何原因。
我当时以为只是搜索接口没处理好特殊字符。结果这只是血案的开始。
二、第一步:换个接口试试
搜索接口异常,可能是它自身的问题。换个接口验证一下。
管理员端有个"系统参数创建"接口,功能简单、参数少、跟搜索完全不搭边:
def test_config_value_emoji(api_session):
api = SysConfigCrudApi(api_session)
resp = api.create(
param_key="autotest_emoji",
param_value="😀😂🤔🎉"
)
assert resp.json()["code"] != "A00005"
也返回了服务器异常。
Emoji 参数值响应: code=A00005, msg=服务器出了点小差
两个完全不同的接口,都对 Emoji 返回同一个异常码。
这就不是"某个接口没写好",是"底层有问题"。
三、第二步:看后端日志
我去看 mvn spring-boot:run 的日志窗口,找 A00005 那行附近的堆栈。
日志里出现了这个:
java.sql.SQLException:
Incorrect string value: '\xF0\x9F\x98\x80\xF0\x9F...'
for column 'param_value' at row 1
翻译一下:
-
\xF0\x9F\x98\x80是 Emoji 😀 的 UTF-8 编码(4 字节) -
Incorrect string value= MySQL 在说"这个字符我存不进去"
问题从"Java 接口异常"变成了"MySQL 存不了 Emoji"。
这是关键转折点。 接下来要确认的不是代码,是数据库。
四、第三步:逐层排除字符集
在开始查之前,我先在脑子里列了一张表,打算一层一层验证,而不是一通乱翻:
| 层级 | 字符集 | 范围 | 状态 |
|---|---|---|---|
| MySQL 服务 | utf8mb4 | 全局默认 | ✅ 正常 |
| yami_shops 库 | utf8mb4 | 新建表默认继承 | ✅ 正常 |
| tz_sys_config 表 | utf8mb3 | 历史建表脚本遗留 | ❌ 异常 |
| tz_prod 表 | utf8mb3 | 历史建表脚本遗留 | ❌ 异常 |
| tz_user 表 | utf8mb3 | 历史建表脚本遗留 | ❌ 异常 |
下面的四次查询,就是在验证这张表。
查询 1:服务级字符集
SHOW VARIABLES LIKE 'character_set%';
character_set_database | utf8mb4
character_set_server | utf8mb4
✅ 服务级没问题。
图 2:服务级字符集 = utf8mb4
查询 2:库级字符集
SELECT DEFAULT_CHARACTER_SET_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = 'yami_shops';
DEFAULT_CHARACTER_SET_NAME
--------------------------
utf8mb4
✅ 库级也没问题。
图 3:库级字符集 = utf8mb4
查询 3:表级字符集
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'yami_shops'
AND TABLE_NAME IN ('tz_sys_config', 'tz_prod', 'tz_user');
TABLE_NAME | TABLE_COLLATION
----------------|------------------
tz_sys_config | utf8mb3_general_ci
tz_prod | utf8mb3_general_ci
tz_user | utf8mb3_general_ci
❌ 发现问题了。表级字符集是 utf8mb3,不是 utf8mb4。
图 4:表级字符集 = utf8mb3(关键发现)
查询 4:确认建表语句
SHOW CREATE TABLE tz_sys_config\G
CREATE TABLE `tz_sys_config` (
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
石锤了。
图 5:建表语句里 CHARSET=utf8mb3
关键理解
表是在数据库升级之前就建好的,不会继承后来改的库级默认字符集。
所以只跑SHOW VARIABLES检查字符集没有意义——那只能证明"服务配置正确",证明不了"数据能写进去"。
五、根因:utf8mb3 是什么鬼
这里要解释一个 MySQL 的历史遗留问题:
| 名称 | 字节数 | 能存什么 |
|---|---|---|
| utf8(旧) | 最多 3 字节 | 拉丁字母 + 中文 |
| utf8mb3 | 最多 3 字节 | 和 utf8 同义(新名字) |
| utf8mb4 | 最多 4 字节 | 所有 Unicode,包括 Emoji |
Emoji 是 4 字节字符,必须用 utf8mb4。
为什么 Mall4j 会这样?
它的初始化 SQL 脚本写于 MySQL 5.5 之前——那时 utf8(后来改名叫 utf8mb3)就是 MySQL 支持的最大字符集。utf8mb4 是 5.5.3 才引入的,很多开源项目的建表脚本从此再没更新过。
这是技术债务,不是 Mall4j 独有的 bug。
六、这个 bug 有多大影响
不只是 Emoji。 麻将牌 🀄、音乐符号 🎵、CJK 扩展区的生僻汉字(如"㛃""𡎚")、部分货币符号 〄——所有 4 字节 Unicode 都会触发。
用户如果名字里带生僻字,注册时一样会返回异常。
这不只是"支持表情包"的问题,是部分用户根本无法使用系统的问题。我给它定了 P1(高)。
七、修复建议
方案 A:改表字符集(推荐,但别在线上直接跑)
ALTER TABLE tz_sys_config
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
ALTER TABLE tz_prod
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
ALTER TABLE tz_user
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
注意三点:
① CONVERT TO 会重写整张表并锁表。tz_prod 这种大表在线上跑会堵死写入——小表原地改,大表建议走 pt-online-schema-change 或 gh-ost,先备份、先在从库验证。
② 排序规则建议用 utf8mb4_unicode_ci(MySQL 8.0 可用 utf8mb4_0900_ai_ci),general_ci 对部分语言的排序处理不准确。
③ 改完用 SHOW FULL COLUMNS FROM 表名 再确认一遍字段级字符集。有些字段上硬编码了字符集,CONVERT TO 不一定覆盖得到。
方案B:改建表脚本
搜索所有建表脚本里的 CHARSET=utf8,改成 CHARSET=utf8mb4。
这一步和方案 A 要一起做,否则下次重建库又回到原点。
❌ 为什么不加方案 C: JDBC 连接参数
很多人第一反应是在 JDBC URL 里加 characterEncoding=utf8mb4。
这个方案是错的。
MySQL JDBC 驱动的 characterEncoding 参数只接受 Java 编码名(如 UTF-8),不接受 MySQL 字符集名(如 utf8mb4)。即使强行指定,驱动也会忽略或报错。
更重要的是:即使 JDBC 层面设置了,表级还是 utf8mb3,Emoji 还是存不进去。
这个问题的修复只能在数据库层面解决,JDBC 层面无能为力。
补充:如果现象是乱码而不是报错
那可能是另一类问题。用 HEX() 看原始字节,对比标准 UTF-8 编码,判断是不是连接层或双重编码导致的。
SELECT HEX(param_value) FROM tz_sys_config WHERE id = 1;
```sql
SELECT HEX(param_value) FROM tz_sys_config WHERE id = 1;
八、为什么我没执行修复
这是测试项目。测试的职责是"发现 + 定位 + 建议",不是"修复"。
我把这条记录为 BUG-004,两条对应用例标记为 xfail:
@pytest.mark.xfail(
reason="BUG-004: Mall4j 后端对 Emoji 处理异常,根因是 MySQL 表级 utf8mb3",
strict=False,
)
def test_search_with_emoji_should_not_crash(buyer_headers):
...
开发修复后去掉 xfail 标记,用例自动变成"回归验证"。
这比我自己偷偷改表更合适——保留了 bug 现场,也留下了复现手段。
删掉失败用例是掩盖问题,自己改代码让用例通过是越权。
九、我从这件事学到的 3 件事
- 一个接口返回异常,先看看别的接口 如果只测搜索接口,我会以为"是搜索没写好"。
两个完全不相关的接口都对同一个输入报同一个错,才说明问题在底层。这种判断能力是排查的核心。
- 从"应用层报错"追到"数据库层根因"
A00005是表象Incorrect string value是日志层utf8mb3是根因
测试的价值不在于"发现异常",在于"把异常压缩到一个可修复的点上"。
- 用 xfail 保留 bug 现场 记录 bug 编号、写明根因、等开发修复后自动回归——比删用例或改代码都有用。
十、如果你也遇到类似问题
排查路径
- 单个接口异常 → 换个接口验证
- 多个接口都异常 → 看后端日志堆栈
- 日志里有
SQLException→ 查数据库字符集 - 按 4 层查:服务级 / 库级 / 表级 / 列级
- 大部分时候,问题在"表级"或"列级"
常用 SQL
-- 服务级
SHOW VARIABLES LIKE 'character_set%';
-- 库级
SELECT DEFAULT_CHARACTER_SET_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = 'your_db';
-- 表级
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db';
-- 列级
SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'your_db'
AND CHARACTER_SET_NAME IS NOT NULL
AND CHARACTER_SET_NAME != 'utf8mb4';
把字符集从 utf8mb3 升级到 utf8mb4,不只是支持 Emoji——还支持生僻汉字、数学符号、古文等所有 4 字节 Unicode。
十一、系列文章
这是 Mall4j 自动化测试系列的第 1 篇,后续会继续写:
第 2 篇:订单状态机测试,一个接口发现 4 个 P0 漏洞
不用等价类,不用边界值。我画了一张状态图,对每一条"不存在的边"写逆向用例,抓到了开发完全没想到的 4 个状态越权漏洞。
第 3 篇:双端电商系统的测试差异分析
同一个功能,管理员端和买家端的测试策略为什么完全不同?
第 4 篇:JMeter 三档梯度压测,发现商品列表性能拐点
200 并发时 P99 响应时间从 26ms 暴涨到 677ms,涨了 26 倍,怎么定位的?
第 5 篇:从 Mall4j 的 3 个角色,聊聊真实项目需要几种自动化脚本
买家、管理员、平台运营,鉴权方式和测试策略完全不同。
相关链接
你在测试中追过哪些"追到根因才恍然大悟"的 bug?欢迎评论区交流 👇