原文地址: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没有测试过,应该也存在该问题)。解决方法:
- 服务端使用utf8字符集编码(由于前面时gbk,改成utf8会出现乱码等很多问题)
- 更改应用不对int字段进行非int数据的插入
- 更改应用不使用prepare statement
- binlog的format设置成row格式
注:该问题已经上报给MariaDB,并且被确认为一个Bug,将在后续的版本中进行修复。
相关推荐
10. **复制与高可用**:MySQL的主从复制功能使得数据可以在多个服务器间同步,提供高可用性和灾难恢复能力。 这份“MySQL的协议分析”PDF文档将详细解释这些知识点,帮助读者理解MySQL协议的每一个细节,从而更好地...
- 字符集:推荐从latin1升级到utf8,甚至进一步升级到gbk,以支持更多的字符集需求。 - 数据库角色:数据库主要用于持久化存储和事务一致性,而不是用于复杂的计算任务。 - 读写分离:通过设置主从复制,将写操作...
10. **Unicode支持**:全面支持UTF-8字符集和其他多字节字符集,满足全球化应用的需求。 压缩包中的“mysql-5.5.18-winx64.msi”是一个Windows Installer文件,用于在Windows操作系统上安装MySQL 5.5.18。安装过程...
8. **改进的字符集支持**:MySQL 4.1增加了对多种字符集的支持,包括多字节字符集,如UTF-8,这使得数据库系统能够更好地处理各种语言和地区的文本数据。 9. **增强的客户端工具**:MySQL 4.1的命令行客户端和MySQL...
GBK字符集主要支持简体中文,而对于其他如越南文等非中文字符,我们需要使用UTF-8这样的多字节字符集。UTF-8可以支持多种语言,包括中文和越南文,因此是处理多语言环境的理想选择。 在描述中提到的代码段展示了...
7. **emoji表情存储**:MySQL 5.5之前不支持emoji,需要将字符集由utf8更改为utf8mb4,后者支持4字节Unicode字符,包括大部分emoji。 8. **大数据量查询优化**:对于大型表的查询,可以通过索引来提高效率。如在tid...
5. **character_set_server**: 这是MySQL服务器的默认字符集,这里设置为utf8mb4,支持Unicode的完整字符集,包括表情符号和其他四字节的UTF-8字符。 6. **max_connections**: 这个参数定义了MySQL允许的最大并发...
MySQL 5.5.49还支持复制技术,允许数据在多个服务器间同步,以实现高可用性和负载均衡。主从复制是常见的架构,其中一台服务器作为主节点接收所有写操作,而其他服务器作为从节点同步主节点的数据。 总的来说,...
这些学习笔记涵盖了IT领域的多个关键知识点,让我们一一深入探讨。 首先,JVM(Java Virtual Machine)是Java程序运行的基础,它是Java平台的核心组成部分。理解JVM的工作原理对于优化Java应用程序性能至关重要。...
主从复制可以将数据同步到一个或多个从节点,提高读取性能和数据安全性;哨兵模式则可以检测主节点的故障,并自动将从节点提升为主节点以保证服务的连续性。 **3.4 发布/订阅模式** Redis内置了发布/订阅(Pub/Sub...
MySQL复制包括主从复制和一主多从复制。主服务器记录所有事务的日志(binlog),从服务器通过I/O线程将这些日志拉取下来,并通过SQL线程执行这些日志,从而保持与主服务器数据的一致性。 #### 15、SQL执行慢的原因...
- 不重复的字符串集合,不保留顺序。 - 适合于需要去重的场景。 5. **Sorted Set(有序集合)** - 与Set类似,但元素按分数(score)排序。 - 分数可以用于实现按权重排序等功能。 #### 三、Redis的持久化 ...
了解其数据类型(String、Hash、List、Set、Sorted Set)及其用法,以及如何设置过期策略、主从复制、事务处理和发布订阅模式,可以提升应用程序的性能和响应速度。 5. **并发编程 - synchronized** synchronized...
- `mb_substr()`:用于截取多字节字符串的一部分。 - 示例:`$substring = mb_substr($string, 0, 5, 'UTF-8');` 以上就是根据提供的 PHP 经典面试题整理出的关键知识点,希望对学习 PHP 有所帮助。
- **IO流**:理解字节流和字符流的区别,以及缓冲流、对象序列化等概念。 - **反射**:用于在运行时检查类的信息,创建和调用对象。 2. **Java进阶** - **设计模式**:例如单例模式、工厂模式、装饰器模式等,...
面试题包含了不同技术层面的面试问题,同时也能对一些没有面试开发经验的小白给予不可估量的包装, 让你的薪水绝对翻倍, 本人亲试有效.Java面试题84集、java面试专属及面试必问课程,所有的面试题有视屏讲解, 解答方案....