GreatSQL迁移中排序规则变化引发乱码的剖析与解决

3周前发布 gsjqwyl
20 0 0

一、引言

有一个旧系统的数据库从Oracle迁移到GreatSQL的过程中,第一批迁移(存储过程、表结构、基础数据)顺利完成。但在第二批数据迁移时出现了主键冲突问题:原来Oracle数据库里存在主键字段A和a(忽略大小写后视为相同值),然而GreatSQL默认的排序规则utf8mb4_0900_ai_ci是不区分大小写的,这就导致了主键冲突。

为了解决这个问题,把排序规则调整为utf8mb4_0900_bin来区分大小写。但调整之后,Java程序读取中文字段时出现了乱码,比如“好”显示成“好”,直接影响了业务功能。本文从环境兼容性、驱动版本、字符编解码机制等角度深入剖析问题的根源,并给出三种解决办法。

二、环境说明与问题背景

关键组件的版本情况:

组件 版本号 备注
数据库 GreatSQL 8.0.32-26 默认字符集utf8mb4
jdk 1.7.0_80 旧版本,升级成本高
驱动版本 mysql-connector-java 5.1.46 官方已停止维护
字符集 utf8mb4 未变动
排序规则 utf8mb4_0900_ai_ci->utf8mb4_0900_bin 变更后引发乱码

核心矛盾点

  • 业务需求:需要使用utf8mb4_0900_bin排序规则来解决主键冲突问题。
  • 环境限制:旧版本的JDK 1.7和低版本的驱动(5.1.46)存在兼容性问题,没办法正确解析新的排序规则。

三、复现过程

1. 创建测试表并插入数据

greatsql> CREATE TABLE test.t1(id int PRIMARY KEY, cname varchar(10)) DEFAULT charset=utf8mb4 collate=utf8mb4_0900_ai_ci;
Query OK, 0 rows affected (0.02 sec)

greatsql> INSERT INTO test.t1 VALUES(1, '好');
Query OK, 1 row affected (0.00 sec)

确认Java版本

$ javac -version
javac 1.7.0_80
$ java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

编写SimpleDBQuery.java,内容如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SimpleDBQuery {
    public static void main(String[] args) {
        String url = "jdbc:mysql://172.17.134.66:3301/test?characterEncoding=UTF-8&useSSL=false";
        String username = "bing";
        String password = "abc123";
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection(url,  username, password);
            String sql = "SELECT cname FROM t1 LIMIT 1";
            stmt = conn.createStatement();
            rs = stmt.executeQuery(sql);
            if (rs.next())  {
                String value = rs.getString("cname");
                System.out.println(" 查询结果: " + value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs!= null) rs.close();
                if (stmt!= null) stmt.close();
                if (conn!= null) conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.Java程序读取数据(正常)

$ javac -cp .:mysql-connector-java-5.1.46.jar SimpleDBQuery.java
$ java -cp .:mysql-connector-java-5.1.46.jar SimpleDBQuery
  查询结果: 好

3.修改排序规则后复现乱码

greatsql> ALTER TABLE test.t1 CONVERT TO charset utf8mb4 COLLATE utf8mb4_0900_bin;
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

再次通过Java程序访问数据库中的汉字,就出现了乱码:

$ java -cp .:mysql-connector-java-5.1.46.jar SimpleDBQuery
  查询结果: 好

四、关键排查过程

1. 数据库端验证

确认表中的数据没有乱码,而且字符集也没有变化,只是排序规则进行了修改。

greatsql> SHOW CREATE TABLE test.t1 \G
*************************** 1. row ***************************
           Table: t1
    Create Table: CREATE TABLE `t1` (
      `id` int NOT NULL,
      `cname` varchar(10) COLLATE utf8mb4_0900_bin DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin
    1 row in set (0.00 sec)

greatsql> SELECT * FROM test.t1;
+----+-------+
| id | cname |
+----+-------+
|  1 | 好    |
+----+-------+
1 row in set (0.01 sec)

2. 驱动源码分析

查看驱动5.1.46中只支持utf8mb4_0900_ai_ci,没有定义utf8mb4_0900_bin。

$ grep -inr 'utf8mb4_0900_ai_ci' *
com/mysql/jdbc/CharsetMapping.java:489:        collation[255] = new Collation(255, "utf8mb4_0900_ai_ci", 0, MYSQL_CHARSET_NAME_utf8mb4);
$ grep -inr 'utf8mb4_0900_bin' *
$ pwd
/opt/software/jdbc_test/mysql-connector-java-5.1.46/src

3. 解码逻辑

当驱动没办法识别排序规则的时候,默认会使用latin1解码,这样就会导致UTF-8字节流被错误解析

image-20250318144141148

4. 网络抓包验证

通过抓包,对比确认不管是utf8mb4_0900_ai_ci,还是utf8mb4_0900_bin,返回的十六进制数据都是 e5 a5 bd

5. 解析抓包内容验证

要是用默认的latin1作为字符集来解码,那么把e5 a5 bd按照latin1来解码,会发现返回的结果集和查询到的乱码一致。

通过在线工具 https://qr9.net/string-encoding 将十六进制内容按latin1解码,发现和乱码内容一致:

通过在线工具 https://lzltool.cn/Tools/HexToUtf8 将十六进制内容按utf8解码,能确认解析出正确的返回结果“好”:

五、根因分析

乱码本质 :低版本的驱动(5.1.46)没有适配GreatSQL 8.0.32的utf8mb4_0900_bin排序规则,触发了默认的latin1解码机制,使得UTF-8字节流被错误转换。

六、解决方法

方案1 :强制指定JDBC字符集参数(推荐)

在连接字符串中明确声明编解码规则:

String url = "jdbc:mysql://10.191.81.31:3307/test?useUnicode=true&characterSetResults=utf8&characterEncoding=utf8&useSSL=false";

参数的作用:

  • characterSetResults=utf8:强制服务端返回UTF-8编码。
  • characterEncoding=utf8:客户端使用UTF-8编码发送请求。

优点:不需要升级,调整简单,兼容性强。

方案2 :使用兼容的排序规则

把排序规则改成utf8mb4_bin(不是utf8mb4_0900_bin),这个规则在驱动5.1.46中已经支持,而且同样区分大小写。

ALTER TABLE test.t1 CONVERT TO CHARSET utf8mb4 COLLATE utf8mb4_bin;  

方案3 :升级驱动至8.0.x版本

使用mysql-connector-java-8.0.32,完全支持utf8mb4_0900_bin

<!-- Maven依赖示例 -->  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.32</version>  
</dependency>  

注意事项:需要验证JDK 1.7和新版驱动的兼容性,部分API可能需要调整。

七、总结

本文通过复现、排查、分析三个步骤定位了乱码问题,根本原因在于驱动版本和数据库排序规则的兼容性。三种解决方案各有适用场景:

  1. 快速修复场景:调整JDBC连接参数,强制UTF-8编解码。

  2. 保守场景:使用兼容的utf8mb4_bin排序规则。

  3. 技术升级场景:升级驱动至8.0.x版本。

建议根据实际环境选择最优方案,并且在变更之后进行全面测试,确保数据一致性和业务功能正常。


Enjoy GreatSQL 😃

关于 GreatSQL

GreatSQL是面向金融级应用的国内自主开源数据库,具备高性能、高可靠、高易用性、高安全等多个核心特性,可以作为MySQL或Percona Server的可选替代,用于线上生产环境,而且完全免费并且兼容MySQL或Percona Server。

相关链接: GreatSQL社区
Gitee
GitHub
Bilibili

GreatSQL社区:

社区博客有奖征稿详情:https://greatsql.cn/thread-100-1-1.html

image-20230105161905827

技术交流群:

微信:扫码添加GreatSQL社区助手微信好友,发送验证信息加群

image-20221030163217640
© 版权声明

相关文章

暂无评论

暂无评论...