(转)MySQL多字节字符集造成主从数据不一致问题

本文分析了在MySQL Master-Slave复制环境中,特定条件下导致的数据不一致性问题。通过对binlog格式、字符集设置及PreparedStatement使用的深入探讨,揭示了问题的根本原因,并提出了有效的解决方案。

原文地址:http://backend.blog.163.com/blog/static/20229412620133274030845/

 问题产生
线上一直有个历史遗留问题,最近DBA提了出来,所以跟了下代码,作了下简单分析,问题描述如下:
在master-slave的环境下,对master上的某个表中的数据插入,会导致master-slave数据不一致的情况,通过反复试验,确定出现该情况的条件如下:

  • master上设置了character_set_server=gbk
  • 应用中采用了prepared statement并且设置了useCursorFetch=true
  • 对于master上某个表中的int字段,采用了字符的形式进行插入

比如,master上有张表,结构如下:

create table t(id int auto_increment primay key, count int)engine=innodb;

进行的操作如下:

conn =DriverManager.getConnection("jdbc:mysql://192.168.0.1:3307/test?useCursorFetch=true&user=root");
pstmt = conn.prepareStatement("insert into test(count) values(?)");
pstmt.setString(1,"1");
pstmt.execute();

通过上面的操作,我们发现,在master上,插入后的结果如下: 

mysql>select*from t;
+----+-------+
| id | count |
+----+-------+
|1|1|
+----+-------+

而在slave上,却变成了另外一个结果: 

mysql>select*from t;
+----+-------+
| id | count |
+----+-------+
|1|49|
+----+-------+

从上面的信息可以看出,master-slave上的数据出现了不一致,查看master上的binlog,我们会发现如下信息:

insert into t(count) values(0x31)

这里的binlog中的值'1'被转化成了16进制0x31。
问题分析
binlog在某种情况下会被转化成16进制存储,这个可能很多人没有注意,转化成16进制有又造成了master-slave上数据不一致,这个就让人比较难以接受了,以下我们的分析就从这两方面入手:
  • MySQL为什么要把Binlog转化成16进制,在那些条件下会转成16进制?
  • 转化成16进制后,数据为什么会出现不一致?


首先,我们来分析第一个问题,通过上面的条件进行跟踪测试,我们很快发现,MySQL把Binlog中的字符串转化成16进制存储的条件有两个:

  • 客户端使用了prepared statement
  • 客户端传过来的编码是多字节符集编码


客户端在使用prepared statement的时候,在执行前,需要先解析出内部给定的参数,由于涉及到转义的问题,需要把参数内部的如\0,\n等字符进行相应的替换(\0会被替换成'\\0'),而对于多字节字符集编码的字符串(如:gbk,gb2312),转义字符'\'可能出现在字符的第二个字节,例如字符串“?\0”,可能实际的编码是两个字符'?\'和‘0’,如果按照替换的原则替换,很有可能会破坏原有的字符串的内容,所以MySQL对于这种转义符'\'可能出现在第二个字节的字符集,都给了一个标识进行说明(escape_with_backslash_is_dangerous=1),在处理时,遇到这种字符集,直接把字符串转成16进制进行处理,这样就可以避免转义出现的问题。
具体的代码可以查看log_event.cc文件中的append_query_string函数,关键代码如下:

if(csinfo->escape_with_backslash_is_dangerous)
ptr= str_to_hex(ptr,from->ptr(),from->length());
else
{
*ptr++='\'';
ptr+= escape_string_for_mysql(csinfo, ptr,0,from->ptr(),from->length());
*ptr++='\'';
}

整个字符串转成16进制的过程在Prepared statement处理时进行,所以在没有使用prepared statement或者客户端字符集不会出现转义符'\'出现在第二字节的情况下,不会产生该问题。
转义符'\'会出现在第二字节的字符集如下:
my_charset_big5_chinese_ci
my_charset_big5_bin
my_charset_cp932_japanese_ci
my_charset_cp932_bin
my_charset_gbk_chinese_ci
my_charset_gbk_bin
my_charset_sjis_japanese_ci
my_charset_sjis_bin
分析了第一个问题,再来看第二个问题,binlog被转成16进制后,为什么主从上的数据会出现不一致?
MySQL中,参数类型的确定在SQL语句的解析中进行,由于prepare statement采用先给出SQL后设置值得形式,所以在解析SQL时,MySQL还不知道具体的值,构造了Item_param对象,然后通过设置,把后面的参数'1'设置给Item_param的str_value成员,而对于salve,binlog传递没有prepared statement的信息,所以在slave上,还是按照一般的方式执行,slave发现value的值为0x31,所以构造一个Item_hex_string对象来保存,所以我们只需要查看下两个Item的save_in_field方法,就可以查明具体的原因,先看master上的处理:
field->store(str_value.ptr(), str_value.length(),str_value.charset());
最终的处理方式为
get_int(cs, from, len, &rnd, UINT_MAX32, INT_MIN32, INT_MAX32);
我们的字段类型为int,而实际给定的值是string,在处理时,需要进行转换,转换的关键代码如下:

for(ul=0; str < end9 &&(ch=(uchar)(*str -'0'))<10; str++)
{
ul= ul *10+ ch;
}

从上面的代码我们可以知道,master这种转化方式类似于C中的atoi函数,例如字符串‘1234'会被转换为1234,如果中间出现非数字字符,后面部分会被截断,所以,在master上的数据就是字符串'1'转换过来的值1
现在看下slave上的处理:
slave上处理转换的方法主要在下面的代码:
nr= (ulonglong) val_int();
函数val_int的处理过程如下:
先检查字段类型,如果是string则按照string的方式处理,如果不是string,则按照下面的方式处理

char*end=(char*) str_value.ptr()+str_value.length(),
*ptr=end-min(str_value.length(),sizeof(longlong));
ulonglong value=0;
for(; ptr !=end; ptr++)
value=(value <<8)+(ulonglong)(uchar)*ptr;

slave上通过把value中的每个字节强制转化得到,所以如果value为‘1234’,通过强制转换过来的值将是:

(uchar)'1'<<24+(uchar)'2'<<16+(uchar)'3'<<8+(uchar)'4'
=31<<24+32<<16+33<<8+34

这样就造成了master-slave上的数据不一致
总结
该问题主要的原因在于MySQL的两种不同的Item在处理字符串转整型的方法不一致,Item_param通过类似于atoi的形式,直接把字符中的数字通过-'0'转换到整型,而Item_hex_string则通过强制内存转化所得,这两种方式都合理,但是两边没有统一,造成replication出错,MySQL从5.1版本后到目前MySQL5.5的版本中都存在该问题(MySQL5.6没有测试过,应该也存在该问题)。解决方法:
  1. 服务端使用utf8字符集编码(由于前面时gbk,改成utf8会出现乱码等很多问题)
  2. 更改应用不对int字段进行非int数据的插入
  3. 更改应用不使用prepare statement
  4. binlog的format设置成row格式

注:该问题已经上报给MariaDB,并且被确认为一个Bug,将在后续的版本中进行修复。

<think>嗯,用户的问题是关于在MySQL中新建字段时无法继承表或数据库默认字符集的情况。我需要先理解这个问题的具体情况。首先,用户提到新建字段无法继承表定义或数据库的默认字符集,这可能意味着他们在创建字段时,字符集没有按预期自动应用,导致字段使用了同的字符集,从而引发乱码或其他问题。 接下来,我需要回忆MySQL字符集的继承机制。通常,MySQL字符集设置是分层次的:数据库级、表级、列级。如果没有显式指定,列会继承表的字符集,表会继承数据库的字符集,而数据库如果没有设置,则继承服务器的默认字符集。所以,用户的问题可能出现在某个层级没有正确设置,或者创建列时覆盖了默认值。 然后,我需要参考用户提供的引用内容。引用[1]提到了两种创建数据表的方法,其中第二种使用SQL语法时,如果表已存在可能会报错。这可能和用户的问题有关联吗?或者用户可能在修改表结构时遇到了类似的问题。引用[3]和[4]涉及数据表设计原则和查询优化,可能和字符集设置的影响有关,比如查询时字符集匹配导致的问题。引用[5]提到主从同步的问题,但确定是否直接相关,除非字符集同步导致复制问题。 用户上一次的问题是关于MySQL缺少配置文件时的处理,现在的问题是关于字符集继承,这可能是因为配置文件中没有正确设置默认字符集,导致新建数据库或表时没有继承预期的设置。比如,如果在配置文件中没有指定character-set-server,那么数据库的默认字符集可能是用户想要的,从而影响后续表和字段的继承。 解决方案方面,首先需要检查各层级的字符集设置:服务器、数据库、表。用户可能需要通过SHOW VARIABLES和SHOW CREATE TABLE来确认当前设置。如果数据库或表的字符集没有正确继承,可能需要显式指定。另外,在ALTER TABLE时,如果没有指定字符集,可能默认使用表的设置,但如果表的设置本身正确,或者创建时没有正确继承,就会有问题。 还需要考虑MySQL的版本差异,比如在8.0中默认字符集是utf8mb4,而旧版本可能是latin1。用户如果没有在配置文件中指定,可能使用了默认值,导致符合预期。同时,创建字段时如果没有显式指定字符集,应该自动继承表的,但如果表的字符集未正确设置,就会出问题。 另外,用户可能遇到的情况是,即使数据库和表设置了正确的字符集,但新增字段时还是用了其他字符集。这可能需要检查具体的ALTER TABLE语句,是否在语句中无意中指定了同的字符集,或者在修改表时表的字符集被更改了。 可能还需要考虑COLLATE的设置,因为字符集和排序规则是关联的,如果排序规则匹配,也可能导致问题。 最后,解决方案应包括检查各层级字符集设置,确保正确继承,以及在ALTER TABLE时显式指定字符集,或者在配置文件中设置默认值。此外,修改现有表和字段的字符集,可能需要使用ALTER TABLE ... CONVERT TO CHARACTER SET来统一调整。</think>--- ### MySQL新建字段无法继承字符集问题的解决方案 当MySQL新建字段未继承表或数据库的默认字符集时,通常与字符集继承规则或配置层级有关。以下是系统性排查与解决方案: --- #### 一、**明确MySQL字符集继承规则** MySQL字符集继承遵循层级关系: - **服务器级** → **数据库级** → **表级** → **字段级** 若未显式指定,字段默认继承表的字符集,表继承数据库的字符集数据库继承服务器默认字符集[^4]。 --- #### 二、**排查当前字符集设置** 通过以下命令确认各层级字符集状态: ```sql -- 查看服务器默认字符集 SHOW VARIABLES LIKE 'character_set_server'; -- 查看数据字符集 SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'your_database'; -- 查看表及字段字符集 SHOW CREATE TABLE your_table; ``` 若发现层级间字符集一致(如数据库为`utf8mb4`但表为`latin1`),需修正继承链[^4]。 --- #### 三、**解决方案:修正字符集继承** ##### 1. **修改数据库/表默认字符集** - **修正数据字符集**: ```sql ALTER DATABASE your_database CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` - **修正表字符集**: ```sql ALTER TABLE your_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` 此操作会将表中所有字段的字符集统一更新(已有字段需显式修改)[^1][^4]。 ##### 2. **新建字段时显式指定字符集** 若表字符集正确但字段未继承,需在`ALTER TABLE`时强制指定: ```sql ALTER TABLE your_table ADD COLUMN new_column VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` ##### 3. **配置文件中强制默认字符集** 在`my.cnf`/`my.ini`中全局设置(需重启MySQL): ```ini [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci [client] default-character-set = utf8mb4 ``` 确保所有新建数据库/表默认继承此配置[^3][^4]。 --- #### 四、**修复已存在字段的字符集** 对于未正确继承字符集的现有字段,手动调整: ```sql ALTER TABLE your_table MODIFY COLUMN existing_column VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` --- #### 五、**常见问题与规避** 1. **隐式字符集覆盖** - **现象**:字段使用表的字符集,但表未继承数据库。 - **规避**:始终通过`SHOW CREATE TABLE`验证表级设置,确保与数据一致。 2. **混合存储引擎的影响** - **说明**:如`MyISAM`与`InnoDB`对字符集的支持可能有差异,建议统一使用`InnoDB`[^4]。 3. **主从同步问题** - **风险**:若主从字符集一致,可能导致数据同步错误(如乱码)[^5]。 - **解决**:确保主从配置文件中`character-set-server`一致。 --- #### 六、**验证步骤** 1. 执行`SHOW CREATE TABLE your_table`,确认字段字符集与表一致。 2. 插入多语言字符(如中文、Emoji),检查是否存储正常。 3. 通过`SELECT HEX(column_name)`验证二进制编码是否符合预期(如`utf8mb4`的Emoji编码为4字节)。 --- ### 相关问题 1. 如何批量修改MySQL所有表的字符集? 2. 为什么MySQL中部分字段允许存储Emoji而其他字段报错? 3. 主从复制环境下如何统一字符集配置? --- #### 引用说明 : 修改表结构时需显式指定字符集以避免继承异常。 [^3]: 配置文件设置可强制全局默认字符集。 [^4]: 字符集继承依赖层级配置,需逐级检查。 [^5]: 主从一致字符集可能导致数据同步异常。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值