MySQL 4字节utf8字符更新失败一例

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: MySQL 4字节utf8字符更新失败一例 业务的小伙伴反映了下面的问题 问题 有一个4字节的utf8字符'????'插入到MySQL数据库中时报错 java.sql.SQLException: Incorrect string value: '\xF0\xA0\x99\xB6' for column 'c_utf8mb4' at row 1 数据库中存放该字符的列已经定义为utf8mb4编码了,但相关的参数character_set_server的值为utf8。

MySQL 4字节utf8字符更新失败一例

业务的小伙伴反映了下面的问题

问题

有一个4字节的utf8字符'????'插入到MySQL数据库中时报错

java.sql.SQLException: Incorrect string value: '\xF0\xA0\x99\xB6' for column 'c_utf8mb4' at row 1 

数据库中存放该字符的列已经定义为utf8mb4编码了,但相关的参数character_set_server的值为utf8。 比较奇怪的是使用mysql-connector-java-5.1.15.jar驱动时没有问题,使用更高版本的驱动如mysql-connector-java-5.1.22.jar,就会出错。JDBC的下面2个连接参数,不过设置与否,都没有影响。

  • characterEncoding=utf8
  • useUnicode=true

原因

jdbc驱动未正确设置SET NAMES utf8mb4导致转码错误。

根据MySQL官方手册,在MySQL Jdbc中正确使用4字节UTF8字符的方法如下:

http://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-14.html:

Connector/J mapped both 3-byte and 4-byte UTF8 encodings to the same Java UTF8 encoding.

To use 3-byte UTF8 with Connector/J set characterEncoding=utf8 and set useUnicode=true in the connection string.

To use 4-byte UTF8 with Connector/J configure the MySQL server with character_set_server=utf8mb4. Connector/J will then use that setting as long as characterEncoding has not been set in the connection string. This is equivalent to autodetection of the character set. (Bug #58232) 

按照MySQL官方手册提供的方法,MySQL JDBC驱动内部会在建立连接时发送SET NAMES utf8mb4给服务端,确保正确进行字符编码。 所以,本问题属于应用未按要求使用MySQL JDBC。但5.1.15可以插入4字节字符也是比较奇怪的事情。 mysql-connector官网的 change log中并且提交5.1.15~5.1.22之间有相关的改动。但是,通过比较代码发现,这部分逻辑确实发生了变更。

5.1.15

com\mysql\jdbc\ConnectionImpl.java:

private boolean configureClientCharacterSet(boolean dontCheckServerMatch)
    throws SQLException
{
...
    if(getEncoding() != null)
    {
        String mysqlEncodingName = CharsetMapping.getMysqlEncodingForJavaEncoding(getEncoding().toUpperCase(Locale.ENGLISH), this);
        if(getUseOldUTF8Behavior())
            mysqlEncodingName = "latin1";
        if(dontCheckServerMatch || !characterSetNamesMatches(mysqlEncodingName))
            execSQL(null, (new StringBuilder()).append("SET NAMES ").append(mysqlEncodingName).toString(), -1, null, 1003, 1007, false, database, null, false);
        realJavaEncoding = getEncoding();
    }
...
} 

给CharsetMapping.getMysqlEncodingForJavaEncoding()传入的参数是UTF-8,对应的mysql的编码有2个,utf8和utf8mb4, 其中utf8mb4优先,所以这个函数返回的mysql编码是utf8mb4。即之后执行了SET NAMES utf8mb4

相关代码:

com\mysql\jdbc\CharsetMapping.java:

public static final String getMysqlEncodingForJavaEncoding(String javaEncodingUC, Connection conn)
    throws SQLException
{
    List mysqlEncodings = (List)JAVA_UC_TO_MYSQL_CHARSET_MAP.get(javaEncodingUC);
    if(mysqlEncodings != null)
    {
        Iterator iter = mysqlEncodings.iterator();
        VersionedStringProperty versionedProp = null;
        do
        {
            if(!iter.hasNext())
                break;
            VersionedStringProperty propToCheck = (VersionedStringProperty)iter.next();
            if(conn == null)
                return propToCheck.toString();
            if(versionedProp != null && !versionedProp.preferredValue && versionedProp.majorVersion == propToCheck.majorVersion && versionedProp.minorVersion == propToCheck.minorVersion && versionedProp.subminorVersion == propToCheck.subminorVersion)
                return versionedProp.toString();
            if(!propToCheck.isOkayForVersion(conn))
                break;
            if(propToCheck.preferredValue)
                return propToCheck.toString();
            versionedProp = propToCheck;
        } while(true);
        if(versionedProp != null)
            return versionedProp.toString();
    }
    return null;
}

...

CHARSET_CONFIG.setProperty("javaToMysqlMappings", "US-ASCII =\t\t\tusa7,US-ASCII =\t\t\t>4.1.0 ascii,...
UTF-8 = \t\tutf8,UTF-8 =\t\t\t\t*> 5.5.2 utf8mb4,..."); 

注:上面的定义UTF-8 =\t\t\t\t*> 5.5.2 utf8mb4中的*代表有多个mysql编码对应于同一个Java编码时,该编码优先

5.1.22

com\mysql\jdbc\ConnectionImpl.java:

private boolean configureClientCharacterSet(boolean dontCheckServerMatch)
    throws SQLException
{
...
     if(getEncoding() != null)
    {
        String mysqlEncodingName = getServerCharacterEncoding();
        if(getUseOldUTF8Behavior())
            mysqlEncodingName = "latin1";
        boolean ucs2 = false;
        if("ucs2".equalsIgnoreCase(mysqlEncodingName) || "utf16".equalsIgnoreCase(mysqlEncodingName) || "utf32".equalsIgnoreCase(mysqlEncodingName))
        {
            mysqlEncodingName = "utf8";
            ucs2 = true;
            if(getCharacterSetResults() == null)
                setCharacterSetResults("UTF-8");
        }
        if(dontCheckServerMatch || !characterSetNamesMatches(mysqlEncodingName) || ucs2)
            execSQL(null, (new StringBuilder()).append("SET NAMES ").append(mysqlEncodingName).toString(), -1, null, 1003, 1007, false, database, null, false);
        realJavaEncoding = getEncoding();
    }
...
}

...
public String getServerCharacterEncoding()
{
    if(io.versionMeetsMinimum(4, 1, 0))
    {
        String charset = (String)indexToCustomMysqlCharset.get(Integer.valueOf(io.serverCharsetIndex));
        if(charset == null)
            charset = (String)CharsetMapping.STATIC_INDEX_TO_MYSQL_CHARSET_MAP.get(Integer.valueOf(io.serverCharsetIndex));
        return charset == null ? (String)serverVariables.get("character_set_server") : charset;
    } else
    {
        return (String)serverVariables.get("character_set");
    }
} 

解决办法

一直使用旧版的5.1.15驱动不是一个好办法,因此在使用新版驱动时,采取以下措施之一解决这个问题。

  1. 参考官网的说明,修改my.cnf

    character_set_server=utf8mb4 
  2. 在应用中获取连接后执行下面的SQL

    stmt.executeUpdate("set names utf8mb4")

补充

 根据5.1.22的MySQL JDBC驱动代码,MySQL JDBC支持utf8mb4需要满足以下2个条件 
 
1. MySQL系统变量`character_set_server`的值为utf8mb4  
2. MySQL JDBC连接参数characterEncoding的值为以下值之一    
     - null
     - UTF8
     - UTF-8 

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
存储 关系型数据库 MySQL
Mysql中utf8和utf8mb4区别
Mysql中utf8和utf8mb4区别
95 0
|
30天前
|
存储 关系型数据库 MySQL
MySQL 字符字段长度设置详解:语法、注意事项和示例
MySQL 字符字段长度设置详解:语法、注意事项和示例
142 0
|
3月前
|
关系型数据库 MySQL
MySQL——删除指定字符
MySQL——删除指定字符
36 1
|
4月前
|
关系型数据库 MySQL
mysql模糊查询指定根据第几个字符来匹配
mysql模糊查询指定根据第几个字符来匹配
223 1
|
6月前
|
存储 关系型数据库 MySQL
MySQL字段的字符类型该如何选择?千万数据下varchar和char性能竟然相差30%🚀
本篇文章来讨论MySQL字段的字符类型选择并深入实践char与varchar类型的区别以及在千万数据下的性能测试
MySQL字段的字符类型该如何选择?千万数据下varchar和char性能竟然相差30%🚀
|
5月前
|
SQL 关系型数据库 MySQL
字节面试:MySQL自增ID用完会怎样?
字节面试:MySQL自增ID用完会怎样?
69 0
字节面试:MySQL自增ID用完会怎样?
|
5月前
|
分布式计算 DataWorks 关系型数据库
DataWorks操作报错合集之在数据集成到MySQL时,遇到特殊字符导致的脏数据如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
5月前
|
存储 自然语言处理 搜索推荐
mysql中utf8、utf8mb4和utf8mb4_unicode_ci、utf8mb4_general_ci
mysql中utf8、utf8mb4和utf8mb4_unicode_ci、utf8mb4_general_ci
122 0
|
6月前
|
消息中间件 关系型数据库 MySQL
MySQL 到 Kafka 实时数据同步实操分享(1),字节面试官职级
MySQL 到 Kafka 实时数据同步实操分享(1),字节面试官职级
|
6月前
|
关系型数据库 MySQL
Mysql 查询以某个字符开头的语句和LIKE的使用
Mysql 查询以某个字符开头的语句和LIKE的使用
99 0