1. 问题背景与发现过程
去年夏天,我们电商平台准备上线一个国际版功能,需要支持多语言商品名称和用户评论。测试阶段一切正常,直到某天运营同事反馈:部分包含emoji表情的用户评论在存入数据库后变成了问号"?"。作为负责数据库维护的DBA,我立即展开了排查。
通过以下命令检查当前数据库字符集配置:
sql复制SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
发现服务端使用的是utf8字符集,而emoji需要4字节编码,utf8最多只支持3字节。这就是问题的根源——MySQL的"utf8"其实是阉割版,真正的UTF-8应该是"utf8mb4"。
2. 字符集升级方案设计
2.1 升级路径选择
我们面临三个可选方案:
- 仅修改新建表的字符集
- 修改数据库默认字符集+转换现有表
- 重建整个数据库实例
经过评估,方案2的综合成本最低。具体考虑因素如下表:
| 评估维度 | 方案1 | 方案2 | 方案3 |
|---|---|---|---|
| 实施复杂度 | 低 | 中 | 高 |
| 对业务影响 | 需要改应用代码 | 需要停机维护 | 需要数据迁移 |
| 长期维护成本 | 高(双字符集) | 低 | 低 |
| 执行时间 | 立即生效 | 2-4小时 | 1-2天 |
2.2 升级前准备工作
-
备份策略:
- 全量备份:
mysqldump -uroot -p --all-databases > full_backup.sql - 二进制日志备份:
FLUSH LOGS;然后备份binlog文件
- 全量备份:
-
影响评估:
sql复制SELECT table_schema, table_name, character_set_name, COUNT(*) as cnt FROM information_schema.columns WHERE character_set_name IS NOT NULL GROUP BY table_schema, table_name, character_set_name; -
停机窗口申请:与业务方协调凌晨2:00-4:00的维护窗口
3. 具体实施步骤
3.1 修改MySQL服务器配置
在my.cnf中添加以下配置:
code复制[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
重要提示:修改配置后必须重启MySQL服务才能生效
3.2 数据库级字符集修改
对每个数据库执行:
sql复制ALTER DATABASE `database_name` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
3.3 表级字符集转换
生成所有表的修改语句:
sql复制SELECT
CONCAT('ALTER TABLE `', table_schema, '`.`', table_name,
'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;') as alter_sql
FROM information_schema.tables
WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');
执行生成的ALTER语句时,需要特别注意:
- 大表需要分批执行
- 有外键约束的表要按依赖顺序执行
- 建议在低峰期执行
3.4 列级字符集检查
即使表级字符集修改后,某些列的字符集可能仍保持原样。需要特别检查:
sql复制SELECT
table_schema, table_name, column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE character_set_name != 'utf8mb4'
AND table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');
4. 遇到的坑与解决方案
4.1 索引长度限制问题
最棘手的坑出现在某张用户表的索引上。执行ALTER时报错:
code复制ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes
原因分析:
- utf8mb4每个字符占4字节,而utf8是3字节
- 原VARCHAR(255)字段在utf8下索引长度为765字节
- 转换为utf8mb4后索引长度变为1020字节
- 多个这样的字段组成的复合索引很容易超过3072字节限制
解决方案:
- 缩短字段长度:
VARCHAR(255)→VARCHAR(191) - 减少索引包含的字段数
- 对于必须的大字段,改为使用前缀索引
4.2 存储过程与函数问题
部分存储过程因字符集不匹配报错。解决方法:
sql复制DELIMITER //
CREATE PROCEDURE `proc_name`()
BEGIN
-- 过程内容
END //
DELIMITER ;
需要在创建时显式指定字符集:
sql复制DELIMITER //
CREATE PROCEDURE `proc_name`()
LANGUAGE SQL
DETERMINISTIC
SQL SECURITY DEFINER
COMMENT ''
BEGIN
-- 过程内容
END //
DELIMITER ;
4.3 应用程序连接问题
部分Java应用出现乱码,原因是JDBC连接字符串需要更新:
code复制jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8
应改为:
code复制jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
5. 验证与监控
5.1 升级后验证
-
基本功能测试:
sql复制CREATE TABLE test_emoji (id INT, comment VARCHAR(100) CHARSET utf8mb4); INSERT INTO test_emoji VALUES (1, '你好😊'); SELECT * FROM test_emoji; -
性能基准测试:
- 查询响应时间
- 写入吞吐量
- 索引扫描效率
5.2 长期监控项
- 存储空间增长:utf8mb4会比utf8多占用约25%的存储空间
- 内存使用:排序缓冲区等内存区域消耗会增加
- 网络传输量:同样数据量的传输时间会略有增加
6. 经验总结与建议
- 新建项目:应该从一开始就使用utf8mb4字符集
- 升级时机:选择业务低峰期,预留足够回滚时间
- 测试策略:
- 先在测试环境完整演练
- 准备详细的回滚方案
- 应用兼容性:
- 检查所有数据库客户端工具
- 验证ORM框架的兼容性
- 性能影响:
- 对于性能敏感系统,需要评估存储和计算开销
- 考虑对历史数据使用压缩存储
这次升级过程中,最大的教训是:MySQL的"utf8"从来就不是完整的UTF-8实现。对于任何需要存储国际化内容的新项目,都应该直接使用utf8mb4字符集。对于存量系统,需要充分评估、周密计划后再执行升级操作。