根本原因是连接层未显式指定charset=utf8mb4,导致客户端与MySQL握手时默认使用latin1等不兼容中文的字符集;必须在连接字符串中强制声明charset=utf8mb4(不可简写为utf8),否则即使表和字段全为utf8mb4,中文也会在传输中被截断成问号。

MySQL连接字符串没设对Charset,中文就变问号
根本原因不是数据库表编码错了,而是客户端连接时没告诉MySQL“我要用utf8mb4传数据”。哪怕表和字段都是utf8mb4_unicode_ci,只要连接层用的是latin1或没声明charset,插入的中文就会在传输途中被截断或乱码,最终存成???或空格。
实操重点是:连接字符串里必须显式指定charset=utf8mb4,且不能只写utf8(MySQL里的utf8实际是阉割版,不支持emoji和部分生僻汉字)。
- Java JDBC:在URL末尾加
?useUnicode=true&characterEncoding=utf8mb4,注意characterEncoding值必须是utf8mb4,不是UTF-8或utf8 - Python PyMySQL:初始化连接时传参
charset='utf8mb4',别漏掉mb4 - .NET MySqlConnector:连接字符串中写
Charset=utf8mb4(区分大小写,不能写成charset) - Node.js mysql2:配置项设
charset: 'utf8mb4',同时确保collation也匹配,比如utf8mb4_unicode_ci
SET NAMES utf8mb4 不能替代连接字符串配置
有人在代码里执行SET NAMES utf8mb4,以为这就够了——其实不够。这条语句只影响当前会话的字符集变量(character_set_client、character_set_results、character_set_connection),但前提是连接建立时底层已支持utf8mb4。如果驱动本身按latin1解析初始握手包,后续再SET也晚了。
典型错误现象:SELECT @@character_set_client, @@character_set_connection, @@character_set_results;返回全是utf8mb4,但插入仍是问号——说明问题出在更早的连接协商阶段。
- 验证方式:用
SHOW VARIABLES LIKE 'character\_set%';查服务端默认值,再对比客户端连接后的真实会话值 -
SET NAMES只能作为临时补救,不能当主力方案 - 某些ORM(如Django)会在首次查询前自动发
SET NAMES,但前提是连接字符串已声明charset,否则ORM无从得知该设什么
MySQL服务端配置也要兜底,但优先级低于连接字符串
如果连接字符串已正确设置charset=utf8mb4,但还是乱码,才需要检查服务端。重点看两个配置项:character_set_server和collation_server,它们决定新建库/表的默认字符集,但不影响已有连接的行为。
常见误操作:改了my.cnf里的default-character-set=utf8mb4(已废弃)或只改init_connect='SET NAMES utf8mb4',却不重启MySQL或没确认生效。
- 必须重启MySQL或执行
SET PERSIST character_set_server = 'utf8mb4';(MySQL 8.0+) -
init_connect对SUPER权限用户无效,所以root连上去不会触发,容易误判配置生效 - 真正起作用的是连接建立时MySQL读取的
character_set_server值,不是my.cnf里写的注释
PHP mysqli_real_escape_string() 乱码本质是字符集不匹配
调用mysqli_real_escape_string()后中文变问号,不是函数bug,而是它依赖当前连接的character_set_client。如果连接字符串没设charset,这个值可能是latin1,导致函数把UTF-8字节流当成latin1解析,转义过程直接损坏字节序列。
修复方式唯一:在mysqli_init()之后、real_connect()之前,用mysqli_options($conn, MYSQLI_OPT_CHARSET_NAME, 'utf8mb4');显式设置,或者更简单——直接在连接字符串里加;charset=utf8mb4(PHP 7.1+)。
- 不要用
mysqli_set_charset($conn, 'utf8mb4')代替连接参数,它可能在连接已建立后再执行,时机太晚 - PDO同理:
new PDO($dsn, $user, $pass, [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'])只是补救,不如DSN里直接写charset=utf8mb4 - 所有转义、预处理、结果集读取都共享同一个连接字符集,设错一个环节,全链路崩
最常被忽略的一点:应用服务器和数据库之间的中间件(比如ProxySQL、MaxScale)也可能重写字符集协商包。如果确认代码和MySQL配置都没问题,却依然乱码,先抓包看TCP流里的initial handshake packet里有没有utf8mb4标识。

















