273 lines
9.7 KiB
Markdown
273 lines
9.7 KiB
Markdown
---
|
||
tags: [MySQL, 字符集, Collation, utf8mb4, utf8mb3, character_set]
|
||
create time: 2026-05-16 00:00
|
||
---
|
||
|
||
# 字符集与排序规则
|
||
|
||
## 概述
|
||
|
||
字符集(Character Set)决定「哪些字符能被存储」,排序规则(Collation)决定「这些字符如何比较和排序」。选错字符集不仅会导致乱码,还可能引发索引失效和安全漏洞。
|
||
|
||
## 字符集速览
|
||
|
||
MySQL 支持超过 80 种字符集,但生产环境中真正有用的只有几个:
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "单字节"
|
||
A["latin1"] -->|"遗留系统"| A1["西欧语言"]
|
||
end
|
||
subgraph "多字节变长"
|
||
B["utf8<br/>MySQL 特有"] -->|"仅支持 BMP"| B1["最多 3 字节"]
|
||
C["utf8mb4"] -->|"完整 UTF-8"| C1["最多 4 字节"]
|
||
end
|
||
subgraph "中文相关"
|
||
D["gbk"] -->|"国标简体"| D1["2 字节"]
|
||
E["gb2312"] -->|"老国标"| E1["2 字节"]
|
||
F["big5"] -->|"繁体(台湾)"| F1["2 字节"]
|
||
end
|
||
subgraph "Unicode 其他"
|
||
G["ucs2"] -->|"UTF-16 BE"| G1["定长 2 字节"]
|
||
H["utf32"] -->|"UTF-32"| H1["定长 4 字节"]
|
||
end
|
||
|
||
style C fill:#00B6BC,color:#fff
|
||
style B fill:#EE5A24,color:#fff
|
||
```
|
||
|
||
### 为什么一定要用 utf8mb4?
|
||
|
||
MySQL 中的 `utf8` 实际上只是 **utf8mb3** —— 它最多支持 3 字节编码,无法表示四字节字符(emoji、生僻汉字、部分符号)。
|
||
|
||
```sql
|
||
-- 用 utf8 插入 emoji 会报错!
|
||
SET NAMES utf8;
|
||
INSERT INTO posts (content) VALUES ('Hello 🚀');
|
||
-- ERROR 1366: Incorrect string value: '\xF0\x9F\x9A\x80'
|
||
|
||
-- 换 utf8mb4 就没有问题
|
||
SET NAMES utf8mb4;
|
||
INSERT INTO posts (content) VALUES ('Hello 🚀'); -- OK ✅
|
||
```
|
||
|
||
> [!WARNING] utf8mb4 的性能影响
|
||
> 相比 latin1,utf8mb4 每个字符多占 1~3 字节。这会带来:
|
||
> - 同样的 VARCHAR 长度存的内容更少
|
||
> - 索引体积增大,缓存命中率下降
|
||
> - 网络传输量增加
|
||
>
|
||
> **建议**:除非明确知道不需要中文/emoji,否则一律使用 utf8mb4。现代 SSD 和内存足够应对这个开销。
|
||
|
||
## 排序规则 Collation
|
||
|
||
排序规则命名格式:`<字符集>_<语言>_<后缀>`,后缀含义:
|
||
|
||
| 后缀 | 含义 | 示例 |
|
||
|------|------|------|
|
||
| `_ci` | Case **I**nsensitive(忽略大小写) | `utf8mb4_general_ci` |
|
||
| `_cs` | Case **S**ensitive(区分大小写) | `utf8mb4_general_cs` |
|
||
| `_bin` | Binary(按字节比较) | `utf8mb4_bin` |
|
||
|
||
> [!NOTE] MySQL 8.0 的新后缀
|
||
> 升级到 8.0 后,你会发现默认排序规则多了两个新后缀:
|
||
> - `_ai_ci` = **A**ccent **I**nsensitive — 忽略重音差异(é = e)
|
||
> - `_as_ci` = **A**ccent **S**ensitive — 保留重音差异(é ≠ e)
|
||
>
|
||
> 这解决了一个历史痛点:`utf8mb4_general_ci` 在处理带重音的拉丁字母时不够精确。
|
||
|
||
### 常见 Collation 对比
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph "不区分大小写 ci"
|
||
GC["utf8mb4_general_ci<br/>⚡ 快速但略不精确"]
|
||
UC["utf8mb4_unicode_ci<br/>📐 基于 Unicode 标准"]
|
||
AC["utf8mb4_0900_ai_ci<br/>✅ MySQL 8.0 默认"]
|
||
end
|
||
|
||
subgraph "区分大小写"
|
||
GS["utf8mb4_general_cs"]
|
||
US["utf8mb4_unicode_cs"]
|
||
AS["utf8mb4_0900_as_ci<br/>Accent/Space Insensitive"]
|
||
end
|
||
|
||
subgraph "严格二进制 bin"
|
||
B2["utf8mb4_bin<br/>直接比较字节值"]
|
||
end
|
||
|
||
IN1["输入 'abc' vs 'ABC'"] --> GC
|
||
IN1 --> UC
|
||
IN1 --> AC
|
||
IN2["输入 'abc' vs 'ABC'"] --> GS
|
||
IN2 --> US
|
||
IN2 --> AS
|
||
IN3["输入 'A' vs 'a'"] --> B2
|
||
|
||
GC --> R1["结果: a = b = c"]
|
||
UC --> R1
|
||
AC --> R1
|
||
GS --> R2["结果: a ≠ A"]
|
||
US --> R2
|
||
AS --> R2
|
||
B2 --> R3["结果: A ≠ a (0x41 ≠ 0x61)"]
|
||
|
||
style AC fill:#00B6BC,color:#fff
|
||
style B2 fill:#EE5A24,color:#fff
|
||
style GC fill:#C44569,color:#fff
|
||
```
|
||
|
||
### 该选哪个 Collation?
|
||
|
||
| 场景 | 推荐 | 理由 |
|
||
|------|------|------|
|
||
| **中文项目** | `utf8mb4_0900_as_ci` | MySQL 8.0 默认排序,基于 ICU 标准 |
|
||
| **英文项目** | `utf8mb4_0900_ai_ci` | AI = Accent Insensitive,忽略重音符号 |
|
||
| **邮箱/用户名** | `utf8mb4_bin` | 严格区分大小写 |
|
||
| **密码存储** | 永远不用 Collation——用 Hash | bcrypt/argon2 |
|
||
|
||
> [!TIP] 一个常见的选型误区
|
||
> 很多人看到中文项目就选 `utf8mb4_unicode_ci`,但在 MySQL 8.0 下推荐优先使用 `utf8mb4_0900_*` 系列。它们基于 ICU 标准,对亚洲语言(中日韩)的排序更准确,性能也更好。`utf8mb4_unicode_ci` 本质上是 Unicode 4.0.0 的实现,而 0900 基于 Unicode 9.0.0。
|
||
|
||
```sql
|
||
-- 设置整个数据库的默认字符集和排序规则
|
||
CREATE DATABASE app_db
|
||
CHARACTER SET utf8mb4
|
||
COLLATE utf8mb4_0900_ai_ci;
|
||
|
||
-- 单个表的覆盖
|
||
CREATE TABLE usernames (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
username VARCHAR(50)
|
||
) ENGINE=InnoDB
|
||
DEFAULT CHARSET=utf8mb4
|
||
COLLATE=utf8mb4_bin; -- 用户名严格区分大小写
|
||
```
|
||
|
||
## 层级关系:字符集 → Collation → 作用域
|
||
|
||
```mermaid
|
||
graph BT
|
||
subgraph "Server 层(全局默认)"
|
||
S["server<br/>utf8mb4_0900_ai_ci"]
|
||
end
|
||
subgraph "Database 层(库级覆盖)"
|
||
DB["database<br/>utf8mb4_unicode_ci"]
|
||
end
|
||
subgraph "Table 层(表级覆盖)"
|
||
T["table<br/>utf8mb4_bin"]
|
||
end
|
||
subgraph "Column 层(列级最高优先级)"
|
||
C["column<br/>utf8mb4_general_ci"]
|
||
end
|
||
|
||
S --> DB --> T --> C
|
||
|
||
INFO["优先级:Column > Table > Database > Server<br/>每层都会覆盖上层的设置"]
|
||
|
||
style S fill:#5F2799,color:#fff
|
||
style DB fill:#3D97BE,color:#fff
|
||
style T fill:#FF9F43,color:#000
|
||
style C fill:#C44569,color:#fff
|
||
style INFO fill:#333,color:#fff
|
||
```
|
||
|
||
查询当前配置:
|
||
|
||
```sql
|
||
-- 查看所有层级的字符集和排序规则
|
||
SHOW VARIABLES LIKE 'character_set_server';
|
||
SHOW VARIABLES LIKE 'collation_server';
|
||
|
||
SHOW CREATE DATABASE app_db;
|
||
SHOW CREATE TABLE users\G
|
||
|
||
-- 当前会话的字符集
|
||
SHOW SESSION VARIABLES LIKE 'character_set%';
|
||
```
|
||
|
||
|
||
## Go 应用层字符集配置
|
||
|
||
光在数据库层面设置 utf8mb4 还不够——连接通道必须一致,否则客户端发送的字节会被服务端用错误的字符集解析,轻则乱码,重则报错。
|
||
|
||
### DSN 中指定 charset
|
||
|
||
```go
|
||
// ✅ 标准写法:DSN 中加上 charset=utf8mb4
|
||
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
|
||
db, err := sql.Open("mysql", dsn)
|
||
```
|
||
|
||
> [!QUESTION] 为什么 DSN 里也要写 charset?
|
||
> MySQL 客户端和服务端建立连接时有一个「握手阶段」,双方会协商使用哪个字符集。如果不在 DSN 中声明,MySQL 驱动会使用服务器默认的字符集。当服务器默认不是 utf8mb4 时(比如 legacy 系统的 latin1),就会出现连接层数据面编码不一致的问题。
|
||
|
||
### 运行时动态切换
|
||
|
||
```go
|
||
// 某个极端场景下需要临时切换会话字符集
|
||
_, err := db.Exec("SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci")
|
||
// ⚠️ 注意:这影响整个连接,在连接池场景下要避免频繁调用
|
||
```
|
||
|
||
> [!WARNING] 连接池中的陷阱
|
||
> `sql.Open` 不会立即创建连接,而是在首次查询时懒加载。如果你的程序启动后从未执行过 SET NAMES,而服务端的 `character_set_server` 恰好不是 utf8mb4 —— 那第一个请求就可能触发乱码。
|
||
>
|
||
> **最佳实践**:在初始化连接池后立刻跑一次健康检查,确保连接已正确初始化。
|
||
|
||
```go
|
||
// 建完连接池后立即验证
|
||
err = db.Ping() // 这一步会实际创建一个连接
|
||
```
|
||
|
||
## utf8 → utf8mb4 迁移指南
|
||
|
||
这是一个经典的线上改造场景。如果你的历史系统用了 MySQL 的 `utf8`(其实是 utf8mb3),逐步迁移到 utf8mb4 的步骤如下:
|
||
|
||
### 迁移步骤
|
||
|
||
```sql
|
||
-- Step 1: 检查当前哪些表用了旧 utf8
|
||
SELECT table_schema, table_name, column_name, character_set_name
|
||
FROM information_schema.columns
|
||
WHERE character_set_name IS NOT NULL
|
||
AND character_set_name != 'utf8mb4'
|
||
ORDER BY table_schema, table_name;
|
||
|
||
-- Step 2: 修改表的默认字符集(不影响已有数据)
|
||
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||
|
||
-- Step 3: 修改数据库默认字符集(新建表会自动继承)
|
||
ALTER DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||
|
||
-- Step 4: 修改应用层 DSN,确保连接层也使用 utf8mb4
|
||
-- user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=True
|
||
```
|
||
|
||
> [!NOTE] ALTER TABLE CONVERT 的影响
|
||
> - 对于小表(万行级别),几乎是瞬时完成
|
||
> - 对于大表(千万行+),`CONVERT TO` 会重建整张表:复制全量数据 → 重建索引 → 替换原表
|
||
> - **建议在低峰期操作**,或使用 `pt-online-schema-change` 进行在线 DDL,避免业务中断
|
||
>
|
||
> 如果只是改默认字符集而不影响现有列定义,可以用:
|
||
> ```sql
|
||
> ALTER TABLE users DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||
> -- 注意:这是 DEFAULT,不改变已有列的字符集!
|
||
> ```
|
||
|
||
### 常见踩坑
|
||
|
||
| 坑 | 现象 | 解法 |
|
||
|----|------|------|
|
||
| **只改了表,没改连接** | 能插入 emoji,但查出来是 `???` | DSN 加 `charset=utf8mb4` |
|
||
| **只改了表默认值,没 CONVERT** | 已有列仍是 utf8mb3 | 必须用 `CONVERT TO` 重新编码 |
|
||
| **GORM AutoMigrate 覆盖** | 手动改了字符集,下次 AutoMigrate 又被还原 | 禁用自动迁移,改用 migration 脚本管理 |
|
||
|
||
### 关联笔记
|
||
|
||
- [[hhs/Redis/02-核心数据类型]] — Redis 的 STRING 类型对多字节字符的处理
|
||
- [[hhs/GORM/02-模型定义]] — GORM 建模时的字符串长度设置要点
|
||
- [[hhs/MySQL/02-SQL核心/06-DDL 建表与结构变更]] — 大表 DDL 的在线变更策略
|
||
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — 隐式转换等更多常见问题
|
||
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog Format(ROW vs STATEMENT)对字符集的影响
|