记一次Java字符集问题排查

问题描述

Oceanbase历史库平台最近接入了一些从Mysql库导入的项目,但是在校验的过程中发现有几列数据不一致的情况,对于一个数据迁移工具来说,这是绝对不能接受的错误,于是我们赶紧去查看了错误日志,发现错误的数据长这样:

怎么看起来一毛一样?我们打印出了两个字符串的unicode看一下:

原来是中间那个不可见字符不一样,那是什么原因导致的呢?

问题分析

迁移程序读取和写入数据的逻辑很简单,对于字符串类型的数据,reader从ResultSet中通过getString方法得到一个Java的String对象,writer把这个String对象通过调用PreparedStatement.setString方法来写入目的端。

这个报错的迁移任务的源端Mysql库使用的是gbk字符集(database和table级别都是),而Oceanbase历史库使用的是utf8(database和table级别都是)。我们的迁移程序没有修改任何字符集设置,也就意味着字符串数据会以gbk编码从mysql server端传输到mysql java client,在getString时会以gbk编码解析成String对象(unicode字符),在setString时,String对象又会从unicode字符编码成utf-8格式写入Oceanbase,在这个过程中虽然有字符集的转化,但应该是无损的。

在Unicode标准中,e000—f8ff这段区间称为Private Use Area,是预留给第三方机构自定义字符的,这一段区间里的code是不会分配给标准unicode字符的。我们在分析问题的过程中发现,Java 1.6和1.7版本的gbk编码对于Private Use Area的处理不一样,其中的一部分code用gbk编码后的值在两个版本里不一致。以我们上面发现的那个不一致的unicode字符(e716)为例,分别用Java 1.6和1.7使用gbk编码(调用getBytes("gbk"))的结果如下:

1.6

1.7

而utf8编码在1.6和1.7上是一致的,同样的unicode字符在两个版本上编码是一样的。基于这个发现,我们分析一下这个不一致具体是怎样发生的。

首先,源端Mysql传回来的字节流是这样:

在我们的迁移平台中,迁移和校验是两个不同的任务,它们可能被两个不同的迁移客户端执行,迁移客户端部署的机器中一部分是Java 1.6一部分是Java 1.7。这个迁移任务正好被一个运行在Java 1.7上的客户端执行,于是这个gbk字节流被解码成了然后编码成utf8写入了Oceanbase:

然后校验任务正好被一个运行在Java 1.6上的客户端执行,校验任务会分别从Mysql端和Oceanbase端读取数据然后进行比对。从Mysql传回来的字节流依然是,但是Java 1.6会把它解码成。而从Oceanbase读出来的就是上面写进去的utf-8字节码,解码出来依然是,于是就产生了不一致。

解决方法

这个问题有两个解决方法

1. 把所有部署迁移客户端的机器都升级到同一个Java版本。

2. 读取的时候发init_sql 设置results使用utf8字符编码,让数据库帮我们做转码,这样可以避免不同Java版本编码不一致的问题。当然如果迁移和校验之间数据库升级了并且编码行为发生了变化,那依然会存在这个问题。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180118G0D63K00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券