MySQL多字节字符集造成主从数据不一致问题
线上一直有个历史遗留问题,最近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没有测试过,应该也存在该问题)。解决方法:
- 服务端使用utf8字符集编码(由于前面时gbk,改成utf8会出现乱码等很多问题)
- 更改应用不对int字段进行非int数据的插入
- 更改应用不使用prepare statement
- binlog的format设置成row格式
注:该问题已经上报给MariaDB,并且被确认为一个Bug,将在后续的版本中进行修复。
MySQL多字节字符集造成主从数据不一致问题的更多相关文章
- mysql 主从 数据不一致
用pt-table-checksum校验数据一致性 Jun 4th, 2013 主从数据的一致性校验是个头疼的问题,偶尔被业务投诉主从数据不一致,或者几个从库之间的 数据不一致,这会令人沮丧.通常我们 ...
- 揭秘MySQL主从数据不一致
前言: 目前MySQL数据库最常用的是主从架构,大多数高可用架构也是通过主从架构演变而来.但是主从架构运行时间长久后容易出现数据不一致的情况,比如因从库可写造成的误操作或者复制bug等,本篇文章将会详 ...
- pt-table-checksum检验主从数据不一致
测试环境:主从架构,操作系统liunx 运行pt-table-checksum需要先安装以下依赖包: yum install perl-IO-Socket-SSL perl-DBD-MySQL per ...
- mysql]一次主从数据不一致的问题解决过程()
问题 要解决问题就是怎么对比不一致,然后在不影响业务的情况下,修复数据不一致的问题,把从库缺少的数据补上 下面是能想到和找到的几个方案 1 从新从0开始同步,虽然对主库的使用没有影响,但是那么大的数据 ...
- [mysql]一次主从数据不一致的问题解决过程
之前一篇: 主从更换ip之后重新建立同步 情况时这样的 昨天晚上主动2个机器都迁移了,然后今天才把主动重新连接上,但是从库的偏移量是从今天当前时刻开始的,也就是说虽然现在主动看似正常,其实是少了昨天的 ...
- 基于GTID Replication主从数据不一致操作
基本的M-S结构 现在master与slave主机数据一致: mysql> select * from t1; +------+ | id | +------+ | 1 | | ...
- mysql 主从数据不一致 Slave_SQL_Running: No 解决方法
在slave服务器上通过如下命令 mysql> show slave status\G; 显示如下情况: Slave_IO_Running: Yes Slave_SQL_Running: No ...
- 使用percona-toolkit校验主从数据的一致性
主从数据校验使用percona-toolkit工具集的以下两个工具(主库上使用): pt-table-checksum #检查主从数据是否一致, pt-table-sync #把主库数据同步到从库 ...
- MySQL 使用pt-table-checksum 检查主从数据一致性 (实例转)
1.基本环境: Mysql版本:5.6.12-log Percona-toolkit:2.2.18 Linux:centos6.5 2.安装 源码安装: # 一些依赖包 yum install per ...
随机推荐
- 基于GCC的openMP学习与测试(2)
一.openMP简单测试 1.简单测试(1) #include<omp.h> #include<time.h> #include<iostream> using n ...
- 使用CodeDOM动态编译一个字符串表达式
由于程序需要,计算的表达式使用字符串传输,这样对运算造成了影响.在程序中直接执行这段表达式可以得到值, 但是使用字符串就没有办法运算了, 所以想到用CodeDOM将这段字符串拼接在代码中编译 类似st ...
- 不借助第三方网站四步实现手机网站转安卓APP
今天本来是帮朋友查看是否在APP里可以点外链的一个测试,做着做来感觉了,就把这个测试优化了一下.好了我们来进入正题. 工具:Android Studio 第一步:新建项目 第二步:拖入控件(WebVi ...
- 1.0 配置 appium + java的环境
1. 配置 appim的环境前面中写到. 2. 在eclipse中配置 java 环境, 具体步骤: <1>:在 eclipse 中新建 一个java 项目. ...
- JavaScript第三课 (循环)
循环语句 !如果至少需要执行一次循环体,就用do … while语句,一般情况下用while语句就可以了. while 语法:一直读取循环到条件为假时停止循环. while(条件) { 语 ...
- Struts2国际化信息机制
国际化信息机制 (三种 Action范围. Package范围. 全局) 1. 全局国际化配置信息文件 全局国际化文件,对所有Action 生效,任何程序都可以访问到,需要在struts.xml 配 ...
- JS弹出下载对话框以及实现常见文件类型的下载
写在前面 JS要实现下载功能,一般都是这么几个过程:生成下载的URL,动态创建一个A标签,并将其href指向生成的URL,然后触发A标签的单击事件,这样就会弹出下载对话框,从而实现了一个下载的功能. ...
- js 设置下拉框的默认值
设置下拉框的默认值,直接在option中增加selected就可以了.但是现在要使用JS来设置它的默认值,代码如下: <select name="aaa" id=" ...
- 华为OJ之最长公共子序列
题目描述: 对于两个给定的字符串,给出他们的最长公共子序列. 题目分析: 1,在之前的博文(http://www.cnblogs.com/yonguo123/p/6711360.html)中我们讨论了 ...
- zTree-已勾选项id传输到action的解决方案
测试jsp <%@ page language="java" contentType="text/html; charset=utf-8" page ...