如前文概述,MySQL Binlog v3以前版本, 二进制日志文件的第一个事件是START_EVENT_V3, 从v4版本开始第一个事件为FORMAT_DESCRIPTION_EVENT(以下简称FD事件),替代掉START_EVENT_V3。具体到MySQL服务器版本来说,MySQL 5.0以前版本二进制日志的第一个事件是START_EVENT_V3,而后续版本的第一个事件都是FD事件,由于目前大部分MySQL都跑在5.0+,所以这里不讨论START_EVENT_V3事件,FD事件可以看成是START_EVENT_V3的继承和扩展,是相通的,有兴趣可以虫github 克隆(clone)BinlogMiner的源代码下来参考。

FD事件比较简单,顺带介绍一下BinlogMiner的程序实现,本文可能很拖沓。

文件I/O是编程语言的基本功能,也是编程的基本功。具体到Java来说,传统方式是通过I/O流来进行文件的读写,如"BufferedInputStream"。Java 1.4版本的开始引入新的IO访问方式Java NIO (New IO) ,其中的MappedByteBuffer允许将一个文件通过FileChannel.map,映射成一个内存缓存,跟简单的来说,映射成一个字节数组,可以通过直接访问数组的方式访问文件。这大大简化了文件的随机读写,而且相较于流方式,更接近人的思考方式,还有极高的读写效率。总的来说Java还是很全面的,对于底层的开发也支持很好,NO.1 霸榜多年还是有道理的。BinlogMiner基于MappedByteBuffer实现的。下面介绍的并非BinlogMiner的代码(因为实际代码需要处理很多细节,不适合来说原理),下面只是举个简单实现能跑的栗子。

只需要简单的3行就可以创建一个MappedByteBuffer:

  1. String binlogFileName = "D:\\build\\binlogs\\5.7.18\\blog.000016";
  2. RandomAccessFile binlogFile = new RandomAccessFile(binlogFileName, "r");
  3. FileChannel binlogFileChannel = binlogFile.getChannel();
  4. MappedByteBuffer blogFileBuffer = binlogFileChannel.map(MapMode.READ_ONLY, 0, binlogFile.length());

最后一行,创建一个将文件(D:\\build\\binlogs\\5.7.18\\blog.000016)映射到MappedByteBuffer,只读模式,范围从第一个字节开始(0),到文件结束(binlogFile.length())。通过MappedByteBuffer.position()可以获取当前的文件访问的位置,通过MappedByteBuffer.position(int position)可以设置当前文件访问的位置,在概述的时候提到,前4个字节是二进制文件的幻数(magic number),将访问指针直接跳过,就到了FD事件开始了。

  1. blogFileBuffer.position(4);
  2. int starPos = blogFileBuffer.position(); //记录事件开始位置

这里需要注意的是MappedByteBuffer的position是一个整型,最大的值是Integer.MAX_VALUE。也就是2GB,如果超过2GB,那么就需要重新映射,如:

  1. blogFileBuffer = binlogFileChannel.map(MapMode.READ_ONLY, new_start_position , new_end_position);

这时候MappedByteBuffer的position就是一个相对偏移,需要根据new_start_position_position来获得绝对偏移量,当然对于Binlog不需要担心这个问题,因为截至当前的MySQL 8版本,二进制文件的最大限制还是1GB。

1. Common header的解析

根据前文,事件的第一部分是通用头,根据官方文档:
https://dev.mysql.com/doc/internals/en/binlog-event-header.html

  1. Binlog header Payload
  2. 4 timestamp
  3. 1 event type
  4. 4 server-id
  5. 4 event-size
  6. if binlog-version > 1:
  7. 4 log pos
  8. 2 flags

接下来代码获取文件头的内容:

1.1.timestamp

前4个字节是该事件的时间戳:

  1. byte[] rawTimestamp = new byte[4];
  2. blogFileBuffer.get(rawTimestamp);

根据文档,timestamp是一个无符号的4字节长度的整型,这里需要注意的2个细节是:
  - Java没有unsigned关键字,没有无符号数,这里需要用第三放的库或者直接实现无符号数。
  - 不同平台,使用不同字节序的概念,如,一个数值,1234,litte-endian存储的是4321,而big-endian存储的是1234。
  - 在Java中,通过4个字节记录一个整型,8个字节记录一个长整型,但在binlog中,为节省空间,提高性能,可能是1个字节,2个字节,3个字节,。。。

在BinlogMiner中,是自己写函数实现的无符号数读取,具体可以看PaserHelper这个工具类,这里就不列出来:

  1. ByteOrder order = ByteOrder.LITTLE_ENDIAN; //little-endian,小字节序,x86平台都是。
  2. long timestampValue = ParserHelper.getUnsignedLong(rawTimestamp, order); //获得了存储的值;
  3.  
  4. //这里获得的timestamp是unix timestamp,是基于秒的,而java的Date是基于毫秒(milliseconds)的,所以这里转换要乘以1000.
  5. Date timestamp = new Date(timestampValue * 1000L);
  6. System.out.println(timestamp.toString()); // 这里就获得了可读的事件日期;

1.2. event type

1个字节的无符号数,说明该事件的类型:

  1. byte[] rawEventType = new byte[1];
  2. blogFileBuffer.get(rawEventType);
  3. int eventTypeValue = ParserHelper.getUnsignedInteger(rawEventType, order);
  4. System.out.print(eventTypeValue);

这里输出的事件类型为15,转换成十六进制为0x0F,根据文档:(https://dev.mysql.com/doc/internals/en/binlog-event-type.html)正好是FD事件。

1.3. server-id

MySQL的服务器id,4字节无符号:

  1. byte[] rawServerId = new byte[4];
  2. blogFileBuffer.get(rawServerId);
  3. long serverId = ParserHelper.getUnsignedInteger(rawServerId, order);

1.4. event-size

该事件的长度(字节数),4字节无符号数;

  1. byte[] rawEventSize = new byte[4];
  2. blogFileBuffer.get(rawEventSize);
  3. long eventSize = ParserHelper.getUnsignedInteger(rawEventSize, order);

1.5. log pos

该事件的结束位置,4字节无符号数,v4版本才包含,v4以前的START_EVENT_V3其实也是可以根据event-size计算得log pos的。

  1. byte[] rawLogPos = new byte[4];
  2. blogFileBuffer.get(rawLogPos);
  3. long logPos = ParserHelper.getUnsignedInteger(rawLogPos, order);

1.6. flags

该事件的标志(https://dev.mysql.com/doc/internals/en/binlog-event-flag.html)

  1. byte[] rawFlags = new byte[2];
  2. blogFileBuffer.get(rawFlags);
  3. long flags = ParserHelper.getUnsignedInteger(rawFlags, order);

至此,事件的通用头分析结束。

2. Event Body

Common Header后就是事件内容(Event Body),先看官方定义:

FORMAT_DESCRIPTION_EVENT:A format description event is the first event of a binlog for binlog-version 4. It describes how the other events are layed out.

  1. Payload:
  2. 2 binlog-version
  3. string[50] mysql-server version
  4. 4 create timestamp
  5. 1 event header length
  6. string[p] event type header lengths

注意:这里的文档写的是Payload,但实际分析的时候会发现,应该理解为前文概述中的post header。实际的playload根据版本不同为1个字节,或者0个字节,用于标识CRC算法,先做个引子,后文会详细说明。

2.1.binlog-version

这里自描述了该二进制日志文件的版本,2个字节无符号数:

  1. byte[] rawBinlogVersion = new byte[2];
  2. blogFileBuffer.get(rawBinlogVersion);
  3. int binlogVersion =ParserHelper.getUnsignedInteger(rawBinlogVersion, order);
  4. System.out.print(binlogVersion); //--> 输出4,v4版本

2.2. mysql-server version

记录了服务器的版本,50个字节,字符串类型。

  1. byte[] rawserverVersion = new byte[50];
  2. blogFileBuffer.get(rawserverVersion);
  3. String serverVersion = new String(rawserverVersion).trim();
  4. System.out.print(serverVersion); //--> 输出 5.7.18-15-log

2.3. create timestamp

二进制文件创建的事件戳。4字节无符号数:

  1. byte[] rawCreateTimestamp = new byte[4];
  2. blogFileBuffer.get(rawCreateTimestamp);
  3. long createTimestampValue = ParserHelper.getUnsignedInteger(rawCreateTimestamp, order);
  4. Date createTimestamp = new Date(createTimestampValue * 1000L);

2.4. event header length

通用头(Common header)的长度, 1个字节,无符号数。

  1. byte[] rawCommonHeaderSize = new byte[1];
  2. blogFileBuffer.get(rawCommonHeaderSize);
  3. int commonHeaderSize = ParserHelper.getUnsignedInteger(rawCommonHeaderSize, order);
  4. System.out.print(commonHeaderSize); //--> 输出是19,除了v1版本,其他版本都是固定的19个字节。

2.5. event type header lengths

这里的是每个事件的post header的长度的字节数组,每一个事件一个字节。
首先这个数组长度不是固定的(每个版本包含的事件数很可能是不同的),还可能包含checksum部分,前文说过,在MySQL 5.6.2版本开始引入Checksum, 这里可能还有Checksum的信息。这部分解析就需要些技巧,首先假设“event type header lengths”一直到事件结束。

  1. int pos = blogFileBuffer.position(); //get current position;
  2. byte[] remainBytes = new byte[logPos - pos]; //根据当前的位置和结束位置,计算出该事件剩余的字节数
  3. blogFileBuffer.get(remainBytes);

那么根据这个数组,FD事件的Post Header的长度是多少?根据官方文档,可以根据(事件的编号-1)作为索引获取:

  1. byte rawFdPostHeaderLength = remainBytes[0x0f - 1]; //FD event = 0x0f
  2. int fdPostHeaderLength = rawFdPostHeaderLength & 0xFF; //这里是byte转无符号int
  3. System.out.println(fdPostHeaderLength); //--> FD的post header长度为95个字节。

可以通过log pos和post header来计算出checksum部分的长度,其中根据官方文档,用1个字节记录checksum类型,剩下的为checksum的值:

  1. long crcSize = logPos - (starPos + 19 + fdPostHeaderLength); // 输出结构--> 5 (1 byte crc type + 4 byte crc value)

所以实际的Post header数组为:

  1. byte[] postHeaderSizeArray = Arrays.copyOfRange(remainBytes, 0, remainBytes.length-5);

2.6. checksum算法

所以checksum的类型,和checksum的值为:

  1. int crcType =remainBytes[postHeaderSizeArray.length] & 0xff;
  2. System.out.println(ParserHelper.getHexString(crcValue)); // --> = 1, 根据文档也就是CRC32
  3. byte[] crcValue = Arrays.copyOfRange(remainBytes, remainBytes.length-(5-1), remainBytes.length);
  4. System.out.println(ParserHelper.getHexString(crcValue));
  5. //--> 本案例的输出为F2FABAC5,可以通过mysqlbinlog比对(0xc5bafaf2), 注意,因为是little-endian平台,这里需要倒过来。

至此,第一个事件分析完毕。最后附上完整代码(注意:ParserHelper来自于BinlogMiner):

  1. import java.io.IOException;
  2. import java.io.RandomAccessFile;
  3. import java.nio.ByteOrder;
  4. import java.nio.MappedByteBuffer;
  5. import java.nio.channels.FileChannel;
  6. import java.nio.channels.FileChannel.MapMode;
  7. import java.util.Arrays;
  8. import java.util.Date;
  9.  
  10. import org.littlestar.mysql.binlog.parser.ParserHelper;
  11.  
  12. public class T2 {
  13. public static void main(String[] args) throws IOException {
  14. String binlogFileName = "D:\\build\\binlogs\\5.7.18\\blog.000020";
  15. RandomAccessFile binlogFile = new RandomAccessFile(binlogFileName, "r");
  16. FileChannel binlogFileChannel = binlogFile.getChannel();
  17. MappedByteBuffer blogFileBuffer = binlogFileChannel.map(MapMode.READ_ONLY, 0, binlogFile.length());
  18.  
  19. //skip 4 bytes image number;
  20. blogFileBuffer.position(4);
  21. int starPos = blogFileBuffer.position();
  22. //Common Header..
  23. byte[] rawTimestamp = new byte[4];
  24. blogFileBuffer.get(rawTimestamp);
  25.  
  26. ByteOrder order = ByteOrder.LITTLE_ENDIAN;
  27. long timestampValue = ParserHelper.getUnsignedLong(rawTimestamp, order);
  28. Date timestamp = new Date(timestampValue * 1000L);
  29.  
  30. byte[] rawEventType = new byte[1];
  31. blogFileBuffer.get(rawEventType);
  32. int eventTypeValue = ParserHelper.getUnsignedInteger(rawEventType, order);
  33.  
  34. byte[] rawServerId = new byte[4];
  35. blogFileBuffer.get(rawServerId);
  36. long serverId = ParserHelper.getUnsignedInteger(rawServerId, order);
  37.  
  38. byte[] rawEventSize = new byte[4];
  39. blogFileBuffer.get(rawEventSize);
  40. long eventSize = ParserHelper.getUnsignedInteger(rawEventSize, order);
  41.  
  42. byte[] rawLogPos = new byte[4];
  43. blogFileBuffer.get(rawLogPos);
  44. int logPos = ParserHelper.getUnsignedInteger(rawLogPos, order);
  45.  
  46. byte[] rawFlags = new byte[2];
  47. blogFileBuffer.get(rawFlags);
  48. int flags = ParserHelper.getUnsignedInteger(rawFlags, order);
  49.  
  50. //Event Body
  51.  
  52. byte[] rawBinlogVersion = new byte[2];
  53. blogFileBuffer.get(rawBinlogVersion);
  54. int binlogVersion = ParserHelper.getUnsignedInteger(rawBinlogVersion, order);
  55.  
  56. byte[] rawserverVersion = new byte[50];
  57. blogFileBuffer.get(rawserverVersion);
  58. String serverVersion = new String(rawserverVersion).trim();
  59.  
  60. byte[] rawCreateTimestamp = new byte[4];
  61. blogFileBuffer.get(rawCreateTimestamp);
  62. long createTimestampValue = ParserHelper.getUnsignedInteger(rawCreateTimestamp, order);
  63. Date createTimestamp = new Date(createTimestampValue * 1000L);
  64.  
  65. byte[] rawCommonHeaderSize = new byte[1];
  66. blogFileBuffer.get(rawCommonHeaderSize);
  67. int commonHeaderSize = ParserHelper.getUnsignedInteger(rawCommonHeaderSize, order);
  68.  
  69. int pos = blogFileBuffer.position(); //get current position;
  70. byte[] remainBytes = new byte[logPos - pos];
  71. blogFileBuffer.get(remainBytes);
  72.  
  73. byte rawFdPostHeaderLength = remainBytes[0x0f - 1]; //FD event = 0x0f
  74. int fdPostHeaderLength = rawFdPostHeaderLength & 0xFF; //
  75. System.out.println(fdPostHeaderLength);
  76. long crcSize = logPos - (starPos + 19 + fdPostHeaderLength); // --> 5 (1 byte crc type + crc value)
  77. byte[] postHeaderSizeArray = Arrays.copyOfRange(remainBytes, 0, remainBytes.length-5);
  78. int crcType =remainBytes[postHeaderSizeArray.length] & 0xff;
  79. byte[] crcValue = Arrays.copyOfRange(remainBytes, remainBytes.length-4, remainBytes.length);
  80. System.out.println(ParserHelper.getHexString(crcValue));
  81.  
  82. binlogFileChannel.close();
  83. binlogFile.close();
  84.  
  85. }
  86. }

MySQL二进制日志分析-代码实现(FORMAT_DESCRIPTION_EVENT)的更多相关文章

  1. MySQL二进制日志分析-概述篇

    MySQL从3.23版本开始引入了二进制日志,用于的数据复制, 二进制日志根据MySQL的版本不同,目前有4个版本: https://dev.mysql.com/doc/internals/en/bi ...

  2. MySQL二进制日志总结

    二进制日志简单介绍 MySQL的二进制日志(binary log)是一个二进制文件,主要用于记录修改数据或有可能引起数据变更的MySQL语句.二进制日志(binary log)中记录了对MySQL数据 ...

  3. MySQL二进制日志功能介绍

    二进制日志记录所有更新数据的SQL语句,其中也包含可能更新数据的SQL语句,例如DELETE语句执行过程中无匹配的行.二进制日志中还包含了与执行SQL语句相关的内容,例如SQL语句执行的时间.错误代码 ...

  4. MySQl Study学习之--MySQl二进制日志管理

    MySQl Study学习之--MySQl二进制日志管理 MySQL二进制日志(Binary Log)   a.它包括的内容及作用例如以下:     包括了全部更新了数据或者已经潜在更新了数据(比方没 ...

  5. mysql 二进制日志后缀数字最大为多少

    之前看到mysql二进制日志后面会加一个以数字递增为结尾的后缀,一直在想当尾数到达999999后会发生什么情况,先查了一下官网,对后缀有这样一句介绍:The server creates binary ...

  6. MySQL二进制日志的备份和恢复

    二进制日志:记录数据库修改的相关操作,作用是即时点回复,主从复制 可以按时间滚动,也可以按大小滚动 server-id:服务器身份标识 一.二进制文件的删除方法,千万不要手动删除 PURGE BINA ...

  7. 删除MySQL二进制日志

    服务器上的120G SSD硬盘空间用了92%,检查后发现,原来是 MySQL的二进制日志没有及时清除,占用了大量的空间, 于是直接用命令:reset master 一把删干净了. 1 reset ma ...

  8. MySQL二进制日志(binary log)总结

    本文出处:http://www.cnblogs.com/wy123/p/7182356.html (保留出处并非什么原创作品权利,本人拙作还远远达不到,仅仅是为了链接到原文,因为后续对可能存在的一些错 ...

  9. 查看mysql二进制日志报错问题

    在排查网站被黑时想通过Mysql二进制日志找出修改字段时间,但是使用mysqlbinlog报错: [root@zfszsw1 bin]# ./mysqlbinlog /opt/mysql-bin.00 ...

随机推荐

  1. Knative 基本功能深入剖析:Knative Serving 之服务路由管理

    导读:本文主要围绕 Knative Service 域名展开,介绍了 Knative Service 的路由管理.文章首先介绍了如何修改默认主域名,紧接着深入一层介绍了如何添加自定义域名以及如何根据 ...

  2. 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    转载注明:http://dwz.win/gHc 最近网上出现一个美团面试题:"一个线程OOM后,其他线程还能运行吗?".我看网上出现了很多不靠谱的答案.这道题其实很有难度,涉及的知 ...

  3. hibernate.validator 与 jackson

    1.使用hibernate.validator校验非空,在FormData类中 name字段上面加@NotEmpty @NotEmpty(message = "姓名必填") pri ...

  4. Linux应用开发自学之路

    前言 在 「关于我 」那篇博文里,朋友们应该知道了我不是科班出身,是由机械强行转行到Linux应用开发方向.下面我就详细向大家介绍自己这一路上的转行历程,希望对大家有所启发. 我是学机械专业的,对于机 ...

  5. Jmeter 02 Jmeter断言之响应断言

    看完上一篇博客,相信大家应该可以使用Jmeter发送HTTP请求了.那么我们既然是要测试,就肯定需要判断结果了.Jmeter对于请求的响应数据提供了几种断言机制,这里大概说一下比较常用的几种断言. 响 ...

  6. C#简单爬取数据(.NET使用HTML解析器ESoup和正则两种方式匹配数据)

    一.获取数据 想弄一个数据库,由于需要一些人名,所以就去百度一下,然后发现了360图书馆中有很多人名 然后就像去复制一下,发现复制不了,需要登陆 此时f12查看源码是可以复制的,不过就算可以复制想要插 ...

  7. ECMAScript---变量

    上上篇我们说到ESMAScript是JS的语法规划,JS中的变量.数据类型.语法规范.操作语句.设计模型等都是ES规定的,现在咱们聊一下JS中的变量和常量 变量(variable) 它不是具体值,只是 ...

  8. python 22 类与对象

    目录 1. 从空间角度研究类 1.1 添加对象的属性: 1.2 添加类的属性: 1.3 类与对象的关系: 2. 类与类直接的关系 2.1 类与类的关系: 2.2 依赖关系 -- 主从之分 2.3 组合 ...

  9. JNI开发流程

    交叉编译 在一个平台上去编译另一个平台上可以执行的本地代码 cpu平台 arm x86 mips 操作系统平台 windows linux mac os 原理 模拟不同平台的特性去编译代码 jni开发 ...

  10. html基础——下拉式菜单

    一个网站能否让用户容易使用该网站往往是由菜单栏体现出来,因为它为网页的大多数页面提供功能入口.一个轻轻的点击以后,即可显示出菜单项,将网站的大部分页面和功能显示出来让用户清楚了解从而用户节约一定的时间 ...