Spark 写 MySQL 遇到 Emoji 字符异常分析


1. 问题现象

Spark 任务写数据到 MySQL 时,如果字段中包含 emoji 字符,insert 会失败。

草稿中的描述是:

spark 写数据到 mysql 包含 emoji 字符导致 insert 失败,
已经将 mysql 的 db、table、column 全部改成了 utf8mb4 还是不行。

这类问题常见异常一般是:

java.sql.SQLException: Incorrect string value

或者:

Incorrect string value: '\xF0\x9F\x98\x84...' for column 'xxx'

\xF0\x9F... 这种字节序列通常就是 emoji 这类 4 字节 UTF-8 字符。

2. 为什么 utf8 不够用

MySQL 里历史上的 utf8 不是完整 UTF-8,它最多只能存 3 字节字符。emoji、部分生僻字、一些特殊符号需要 4 字节 UTF-8 才能表示。

MySQL 提供的完整 4 字节 UTF-8 字符集是:

utf8mb4

所以如果要存 emoji,至少要保证:

  1. database 是 utf8mb4
  2. table 是 utf8mb4
  3. column 是 utf8mb4
  4. JDBC 连接会话也是 utf8mb4

很多时候问题就出在第四点:库、表、字段都改了,但连接会话还是 utf8latin1

3. 如何确认问题

先看表字段定义:

SHOW FULL COLUMNS FROM table_name;

重点看 Collation

utf8mb4_general_ci
utf8mb4_unicode_ci
utf8mb4_0900_ai_ci

再看表默认字符集:

SHOW CREATE TABLE table_name;

如果看到:

DEFAULT CHARSET=utf8mb4

说明表层面是对的。

然后看当前连接会话字符集:

SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';

重点关注:

character_set_client
character_set_connection
character_set_results
collation_connection

如果这里还是:

utf8
latin1

即使表字段是 utf8mb4,通过这个连接写入 emoji 仍然可能失败。

4. Spark JDBC 写 MySQL 时容易忽略的地方

Spark 写 MySQL 通常是这样:

df.write
  .format("jdbc")
  .option("url", "jdbc:mysql://mysql-host:3306/test")
  .option("dbtable", "table_name")
  .option("user", "user")
  .option("password", "password")
  .mode("append")
  .save()

如果 JDBC URL 里没有明确字符集,或者使用了老版本 MySQL Connector/J,就可能出现连接字符集和表字符集不一致。

可以把 URL 调整为:

jdbc:mysql://mysql-host:3306/test?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_unicode_ci

如果是 MySQL 8,可以使用:

jdbc:mysql://mysql-host:3306/test?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_0900_ai_ci

注意这里的 characterEncoding 写的是 Java 侧字符集名称 UTF-8,不是 MySQL 字符集名称 utf8mb4utf8mb4 更适合通过 MySQL 的 charset/collation 配置来控制。

5. 解决方案

5.1 修改库字符集

ALTER DATABASE db_name
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

5.2 修改表字符集

ALTER TABLE table_name
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

5.3 确认字段字符集

如果只改表默认字符集,但字段是历史字段,有时字段自身仍然不是 utf8mb4

可以单独修改字段:

ALTER TABLE table_name
MODIFY content VARCHAR(1000)
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

5.4 修改 JDBC URL

Spark 写入时配置:

val url =
  "jdbc:mysql://mysql-host:3306/db_name" +
  "?useUnicode=true" +
  "&characterEncoding=UTF-8" +
  "&connectionCollation=utf8mb4_unicode_ci" +
  "&useSSL=false"

df.write
  .format("jdbc")
  .option("url", url)
  .option("dbtable", "table_name")
  .option("user", "user")
  .option("password", "password")
  .mode("append")
  .save()

5.5 升级 MySQL Connector/J

如果项目使用的是很老的 MySQL 驱动,也建议升级。

Maven 依赖示例:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version>
</dependency>

老版本驱动对字符集、时区、SSL、认证方式的处理都容易遇到兼容性问题。

6. 应急处理方式

草稿里提到最后“改掉了 emoji”,这是一个可用但不完整的应急方案。

如果业务允许,可以在 Spark 写入前清洗 emoji:

import org.apache.spark.sql.functions.regexp_replace

val cleanedDf = df.withColumn(
  "content",
  regexp_replace(col("content"), "[\\x{10000}-\\x{10FFFF}]", "")
)

但是这种方式会丢失原始文本信息。如果字段用于用户评论、昵称、消息内容,一般不建议直接删除 emoji。更好的方式还是修正 MySQL 字符集和 JDBC 连接。

7. 排查顺序

建议按下面顺序排查:

步骤 命令 目的
1 SHOW FULL COLUMNS FROM table_name 确认字段 collation
2 SHOW CREATE TABLE table_name 确认表默认字符集
3 SHOW VARIABLES LIKE 'character_set%' 确认连接会话字符集
4 检查 JDBC URL 确认连接端没有使用错误字符集
5 检查驱动版本 排除老驱动兼容问题
6 单独插入 emoji 测试 确认 MySQL 是否能直接写入

测试 SQL:

INSERT INTO table_name(content) VALUES ('hello 😄');

如果 MySQL 客户端能写,Spark 不能写,重点排查 Spark JDBC URL 和驱动版本。

如果 MySQL 客户端也不能写,重点排查库、表、字段、连接会话字符集。

8. 总结

Spark 写 MySQL 遇到 emoji 失败,本质上通常不是 Spark 写入逻辑问题,而是 MySQL 字符集链路没有完整切到 utf8mb4

只改下面这些还不一定够:

database
table
column

还要检查:

JDBC URL
MySQL Connector/J 版本
character_set_client
character_set_connection
character_set_results
collation_connection

如果业务允许,删除 emoji 可以作为临时方案;如果要保留原始文本,就应该把 MySQL 和 JDBC 连接完整改成支持 utf8mb4 的链路。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录