执子之手

与子偕老


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

  • 搜索
close

MySQL如何保存emoji字符

时间: 2021-07-22   |   分类: 开发     |   阅读: 2227 字 ~5分钟   |   访问: 0

1. 问题

1.1 发现问题

最近生产环境日志中报了一个异常:

 12021-07-11 20:54:41.632 ERROR 26289 --- [XNIO-1 task-11] c.e.t.s.t.mp.WxMpMessageRouterService    :
 2### Error updating database.  Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1
 3### The error may exist in com/eveus/tap/account/mapper/AccountMapper.java (best guess)
 4### The error may involve com.eveus.tap.account.mapper.AccountMapper.insert-Inline
 5### The error occurred while setting parameters
 6### SQL: INSERT INTO tap_account  ( open_id, avatar, nick_name, country, province, city, gender, secret,         status, IS_DISABLED,     is_subscribe, create_time, update_time )  VALUES
 7  ( ?, ?, ?, ?, ?, ?, ?, ?,         ?, ?,     ?, ?, ? )
 8### Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1
 9; uncategorized SQLException; SQL state [HY000]; error code [1366]; Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1; nested exception is java.sql.SQLException:
10 Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1
11
12org.springframework.jdbc.UncategorizedSQLException:
13### Error updating database.  Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1
14### The error may exist in com/eveus/tap/account/mapper/AccountMapper.java (best guess)
15### The error may involve com.eveus.tap.account.mapper.AccountMapper.insert-Inline
16### The error occurred while setting parameters
17### SQL: INSERT INTO tap_account  ( open_id, avatar, nick_name, country, province, city, gender, secret,         status, IS_DISABLED,     is_subscribe, create_time, update_time )  VALUES
18  ( ?, ?, ?, ?, ?, ?, ?, ?,         ?, ?,     ?, ?, ? )
19### Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1
20; uncategorized SQLException; SQL state [HY000]; error code [1366]; Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1; nested exception is java.sql.SQLException:
21 Incorrect string value: '\xF0\x9F\xA6\x84' for column 'NICK_NAME' at row 1
22        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:89)
23        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
24        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
25        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:88)
26        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
27        at com.sun.proxy.$Proxy134.insert(Unknown Source)
28        at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:271)
29        at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:60)
30        at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:96)
31        at com.sun.proxy.$Proxy186.insert(Unknown Source)
32        at com.baomidou.mybatisplus.extension.service.IService.save(IService.java:59)
33        at com.eveus.tap.account.service.impl.AccountServiceImpl.addAccount(AccountServiceImpl.java:151)
34        at com.eveus.tap.account.service.impl.AccountServiceImpl.ensureLoad(AccountServiceImpl.java:226)
35        at com.eveus.tap.account.service.impl.AccountServiceImpl$$FastClassBySpringCGLIB$$34fe95c8.invoke(<generated>)
36        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
37        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769)
38        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
39        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
40        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
41        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
42        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
43        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
44        at com.eveus.tap.account.service.impl.AccountServiceImpl$$EnhancerBySpringCGLIB$$cbb1e26b.ensureLoad(<generated>)
45        at com.eveus.tap.server.taxer.mp.handler.SubscribeHandler.handle(SubscribeHandler.java:54)
46        at me.chanjar.weixin.mp.api.WxMpMessageRouterRule.service(WxMpMessageRouterRule.java:226)
47        at me.chanjar.weixin.mp.api.WxMpMessageRouter.route(WxMpMessageRouter.java:203)
48        at me.chanjar.weixin.mp.api.WxMpMessageRouter.route(WxMpMessageRouter.java:154)
49        at me.chanjar.weixin.mp.api.WxMpMessageRouter.route(WxMpMessageRouter.java:233)

看错误信息是因为 MySQL 字符编码不正确导致无法保存特殊字符导致的。把字符还原一下,发现 \xF0\x9F\xA6\x84 代表的字符内容是:🦄,是一个 Emoji 字符。

当使用对 utf8 编码的时候,和一般汉字占用3个字节不同,Emoji 字符的编码会比较特殊一点,占用4个字节。而由于历史的原因,MySQL 使用的 utf8 编码最长支持3个字节,如果要保存4个字节的 unicode,则需要使用 utf8mb4 编码。上述错误应该是因此而出。

1.2 寻找原因

让我奇怪的是,这个问题应该不会出现才对啊,因为这个表需要存储从微信获取的用户昵称,当时已经考虑了可能存在 Emoji 字符,因此该表的编码已经被修改成了 utf8mb4 。查看一下表结构,现有的表结构定义如下:

1CREATE TABLE `tap_account` (
2  `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
3  `AVATAR` varchar(256) DEFAULT NULL COMMENT '头像,微信授权,用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像)',
4  `NICK_NAME` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
5  ...
6  PRIMARY KEY (`ID`),
7  UNIQUE KEY `tap_account_OPEN_ID_uindex` (`OPEN_ID`)
8) ENGINE=InnoDB AUTO_INCREMENT=72 DEFAULT CHARSET=utf8mb4 COMMENT='自然人账号表';

可以看到目前的字符集已经配置成了 utf8mb4 ,按理说应该能够保存成功才对。

为了验证一下数据库是否修改正确,直接使用sql修改记录试试:

1update tap_account set nick_name='千面怪🦄' where id=1;

发现更新确实可以成功,没有错误发生;而且也可以正常查询到的修改后的结果。这说明表方面应该没有问题。

因为之前考虑到其他表没有存储 Emoji 表情的需要,所以没有在数据库层面整体修改编码。也就是有一部分表还是使用 utf8 编码,只有这个表 tap_account 使用了 utf8mb4 编码。看一下 MySQL 的系统变量:

 1MySQL [tap_admin]> show variables like '%character%';
 2+--------------------------+---------+
 3| Variable_name            | Value   |
 4+--------------------------+---------+
 5| character_set_client     | utf8mb4 |
 6| character_set_connection | utf8mb4 |
 7| character_set_database   | utf8    |
 8| character_set_filesystem | binary  |
 9| character_set_results    | utf8mb4 |
10| character_set_server     | utf8    |
11| character_set_system     | utf8    |
12| character_sets_dir       |         |
13+--------------------------+---------+
148 rows in set (0.001 sec)

可以看到服务器默认的编码(character_set_server)为 utf8 。

是不是因为这个原因导致的问题呢?

2. 关于编码自动检测

我使用的 MySQL 库为 mysql-connector-java,版本为 8.0.19。配置的连接字符串如下:

1spring:
2  datasource:
3    url: jdbc:mysql://rm-uecp.mysql.rds.aliyuncs.com:3306/tap_admin?useUnicode=true&serverTimezone=GMT%2b8:00

连接字符串中只是使用了useUnicode开启了unicode支持,并没有指定具体的字符编码。这时会用到 mysql-connector-java 的自动检测机制。根据 MySQL 官方文档的说明,这个检测机制会用到 character_set_server 这个参数。

The character encoding between client and server is automatically detected upon connection (provided that the Connector/J connection properties characterEncoding and connectionCollation are not set). You specify the encoding on the server using the system variable character_set_server (for more information, see Server Character Set and Collation). The driver automatically uses the encoding specified by the server. For example, to use the 4-byte UTF-8 character set with Connector/J, configure the MySQL server with character_set_server=utf8mb4, and leave characterEncoding and connectionCollation out of the Connector/J connection string. Connector/J will then autodetect the UTF-8 setting.

当在连接字符串中没有设置 charcterEncoding 的时候,将自动使用MySQL系统变量 character_set_server 中指定的字符集。如果需要覆盖以上检测机制,可以指定 characterEncoding 变量:

To override the automatically detected encoding on the client side, use the characterEncoding property in the connection URL to the server. Use Java-style names when specifying character encodings. The following table lists MySQL character set names and their corresponding Java-style names:

特别的对于 characterEncoding=utf8 ,不同版本的 mysql-connector-java 处理上是有些区别的: -w1063 简单来说:8.0.13及以上版本,会自动使用 utf8mb4;而之前版本使用的则是 utf8mb3(也就是通常的utf8编码)。

3. 原因分析

现在来看一下为什么数据库能够保存 Emoji 字符,但是通过 Java 却无法保存。

3.1 utf8mb4 支持条件

首先总结一下支持 utf8mb4 需要的前置条件。

3.1.1 数据库版本

utf8mb4 需要的最低mysql版本为 5.5.3+,若不是,则需要升级。可以使用以下 SQL 查询一下当前的版本号:

1select version();

3.1.2 修改数据库、表或字段的字符集

可以使用以下SQL修改数据库、表或字段支持 utf8mb4 编码:

1-- 修改数据库编码
2ALTER DATABASE database_name CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
3-- 修改表编码
4ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
5-- 修改字段编码
6ALTER TABLE table_name CHANGE column_name VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

经过实验,确认可以只修改表支持 utf8mb4 即可,数据库层面可以保持 utf8 编码不用修改。

3.1.3 MySQL客户端

mysql connector需要至少大于5.1.13,否则无法支持 utf8mb4。另外还要注意8.0.13前后支持形式有所区别。

3.1.4 链接字符串

需要在 mysql-connector-java 链接字符串中增加 useUnicode=true&characterEncoding=utf8mb4 来开启 utf8mb4 支持。

3.2 结论

我用的mysql-connector-java版本是 8.0.19,大于8.0.13,所以使用 characterEncoding=utf8 就相当于支持了 utf8mb4 编码。

但是因为我没有将整个数据库的编码改为 utf8mb4 ,所以不指定该参数的时候会被自动检测为 utf8 编码,导致保存失败。

最后将连接字符串改成这样问题就解决了:

1spring:
2  datasource:
3    url: jdbc:mysql://rm-uecp.mysql.rds.aliyuncs.com:3306/tap_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

4. 总结

总结一下,这个问题出现的原因是因为没有更改数据库的字符编码,导致 mysql-connector-java 的自动检测机制做出了错误的假设,没有设置为需要的 utf8mb4 编码。

另外从网上查到的资料一般说的都是同时修改数据库、表的字符集,没有我这种只修改表的字符集,而数据库字符集没更改的情况。遇到这种特殊情况还是要自己多做实验,实践出真知。

附录、参考资料

  • mysql/Java服务端对emoji的支持
  • RDS MySQL字符集相关说明
  • Chapter 10 Character Sets, Collations, Unicode
  • 6.7 Using Character Sets and Unicode
#Spring# #Java# #MyBatis# #MySQL#
Vue进行条件编译发布
MyBatis Plus中主键生成方式ASSIGN_ID的算法分析
  • 文章目录
  • 站点概览
Orchidflower

Orchidflower

Do one thing at a time, and do well.

76 日志
6 分类
83 标签
GitHub 知乎 OSC 豆瓣
  • 1. 问题
    • 1.1 发现问题
    • 1.2 寻找原因
  • 2. 关于编码自动检测
  • 3. 原因分析
    • 3.1 utf8mb4 支持条件
      • 3.1.1 数据库版本
      • 3.1.2 修改数据库、表或字段的字符集
    • 3.1.3 MySQL客户端
    • 3.1.4 链接字符串
  • 3.2 结论
  • 4. 总结
  • 附录、参考资料
© 2009 - 2022 执子之手
Powered by - Hugo v0.104.3
Theme by - NexT
ICP - 鲁ICP备17006463号-1
0%