查看原文
其他

研发应该懂的binlog知识(下)

孤独烟 孤独烟 2019-04-06

引言

这篇是《研发应该懂的binlog知识(上)》的下半部分。在本文,我会阐述一下 binlog的结构,以及如何使用 java来解析 binlog。 不过,话说回来,其实严格意义上来说,研发应该还需要懂如何监听binlog的变化。我本来也想写这块的知识,但是后来发现,这块讲起来篇幅过长,需要从 mysql的通讯协议开始讲起,实在是不适合放在这篇文章讲,所以改天抽时间再写一篇监听 binlog变化的文章。

说到这里,大家可能有一个疑问:

研发为什么要懂得如何解析binlog?

说句实在话,如果在实际项目中遇到,我确实推荐使用现成的jar包来解析,比如 mysql-binlog-connector-java或者 open-replicator等。但是呢,这类jar包解析 binlog的原理都是差不多的。因为我有一个怪癖,我用一个jar包,都会去溜几眼,看一下大致原理,所以想在这个部分把如何解析 binlog的实质性原理讲出来,希望大家有所收获。大家懂一个大概的原理即可,不需要自己再去造轮子。另外,注意了,本文教你的是解析 binlog的方法,不可能每一个事件带你解析一遍。能达到举一反三的效果,就是本文的目的。

什么,你还没碰到过解析binlog的需求?没事,那先看着,就当学习一下,将来一定会遇到。

正文

先说一下, binlog的结构。 文件头由一个四字节 MagicNumber构成,其值为1852400382,在内存中就是"0xfe,0x62,0x69,0x6e"。这个 MagicNumber就是来验证这个 binlog文件是否有效 。 引一个题外话

java里头的class文件,头四个字节的Magic Number是多少? 回答:"0xCAFEBABE。"这个数字可能比较难记,记(咖啡宝贝)就好。

下面写个程序,读一份 binlog文件,给大家 binlog看看头四个字节是否为"0xfe,0x62,0x69,0x6e",代码如下

  1. public class MagicParser {

  2.    public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};

  3.    public static void main(String[] args)throws Exception {

  4.        String filePath = "D:\\mysql-bin.000001";

  5.        File binlogFile = new File(filePath);

  6.        ByteArrayInputStream inputStream = null;

  7.        inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));

  8.        byte[] magicHeader = inputStream.read(4);

  9.        System.out.println("魔数\\xfe\\x62\\x69\\x6e是否正确:"+Arrays.equals(MAGIC_HEADER, magicHeader));

  10.    }

  11. }

输出如下

  1. 魔数\xfe\x62\x69\x6e是否正确:true

在文件头之后,跟随的是一个一个事件依次排列。在《binlog二进制文件解析》一文中,将其分为三个部分:通用事件头(common-header)、私有事件头(post-header)和事件体(event-body)。本文修改了一下,只用两个Java类来修饰 binlog中的事件,即 EventHeaderEventData。可以理解为下述的对应关系:

  1. EventHeader --> 通用事件头(common-header)

  2. EventData ---> 私有事件头(post-header)和事件体(event-body)

于是,你们可以把Binlog的文件结构像下面这么理解

说一下这个 Checksum,在获取event内容的时候,会增加4个额外字节做校验用。mysql5.6.5以后的版本中binlogchecksum=crc32,而低版本都是binlogchecksum=none。如果不想校验,可以使用set命令设置set binlog_checksum=none。说得再通俗一点, Checksum要么为4个字节,要么为0个字节。

下面说一下通用事件头的结构,如下所示

从上表可以看出,EventHeader固定为19个字节,为此我们构造下面的类,来解析这个通用事件头

  1. public
    class EventHeader {

  2.    private long timestamp;

  3.    private int eventType;

  4.    private long serverId;

  5.    private long eventLength;

  6.    private long nextPosition;

  7.    private int flags;

  8.    //省略setter和getter方法

  9.    @Override

  10.    public String toString() {

  11.        final StringBuilder sb = new StringBuilder();

  12.        sb.append("EventHeader");

  13.        sb.append("{timestamp=").append(timestamp);

  14.        sb.append(", eventType=").append(eventType);

  15.        sb.append(", serverId=").append(serverId);

  16.        sb.append(", eventLength=").append(eventLength);

  17.        sb.append(", nextPosition=").append(nextPosition);

  18.        sb.append(", flags=").append(flags);

  19.        sb.append('}');

  20.        return sb.toString();

  21.    }

  22. }

OK,接下来,我们来一段代码试着解析一下第一个事件的 EventHeader,代码如下所示

  1. public class HeaderParser {

  2.    public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};

  3.    public static void main(String[] args)throws Exception {

  4.        String filePath = "D:\\mysql-bin.000001";

  5.        File binlogFile = new File(filePath);

  6.        ByteArrayInputStream inputStream = null;

  7.        inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));

  8.        byte[] magicHeader = inputStream.read(4);

  9.        if(!Arrays.equals(MAGIC_HEADER, magicHeader)){

  10.            throw new RuntimeException("binlog文件格式不对");

  11.        }

  12.        EventHeader eventHeader = new EventHeader();

  13.        eventHeader.setTimestamp(inputStream.readLong(4) * 1000L);

  14.        eventHeader.setEventType(inputStream.readInteger(1));

  15.        eventHeader.setServerId(inputStream.readLong(4));

  16.        eventHeader.setEventLength(inputStream.readLong(4));

  17.        eventHeader.setNextPosition(inputStream.readLong(4));

  18.        eventHeader.setFlags(inputStream.readInteger(2));      

  19.        System.out.println(eventHeader);

  20.    }

  21. }

输出如下

  1. EventHeader{timestamp=1536487335000, eventType=15, serverId=1,

  2. eventLength=119, nextPosition=123, flags=1}

注意看,两个参数

  1. eventLength=119

  2. nextPosition=123

下一个事件从123字节开始。这是怎么算的呢,当前事件是119字节,算上最开始4个字节的魔数占位符,那么119+4=123。再强调一次,这里的事件长度119字节,是包含EventHeader,EventData,Checksum,三个部分的长度为119。 最重要的一个参数

  1. eventType=15

我们去下面的地址 

https://dev.mysql.com/doc/internals/en/binlog-event-type.html

查询一下,15对应的事件类型为 FORMAT_DESCRIPTION_EVENT。我们接下来,需要知道 FORMAT_DESCRIPTION_EVENT所对应 EventData的结构。在下面的地址 

https://dev.mysql.com/doc/internals/en/format-description-event.html

查询得到 EventData的结构对应如下表所示

ps:这个n其实我们可以推算出,为39。事件长度为119字节,减去事件头19字节,减去末位的4字节(末位四个字节循环校验码),减去2个字节的binlog版本,减去50个字节的服务器版本号,减去4个字节的时间戳,减去1个字节的事件头长度。得到如下算式 

 不过,我们还是假装不知道n是多少吧。

根据上表结构 ,我们给出一个JAVA类如下所示

  1. public class FormatDescriptionEventData {

  2.    private int binlogVersion;

  3.    private String serverVersion;

  4.    private long timestamp;

  5.    private int headerLength;

  6.    private List headerArrays = new ArrayList<Integer>();

  7.    //省略setter和getter方法

  8.    @Override

  9.    public String toString() {

  10.        final StringBuilder sb = new StringBuilder();

  11.        sb.append("FormatDescriptionEventData");

  12.        sb.append("{binlogVersion=").append(binlogVersion);

  13.        sb.append(", serverVersion=").append(serverVersion);

  14.        sb.append(", timestamp=").append(timestamp);

  15.        sb.append(", headerLength=").append(headerLength);

  16.        sb.append(", headerArrays=").append(headerArrays);

  17.        sb.append('}');

  18.        return sb.toString();

  19.    }  

  20. }

那如何解析呢,如下所示

  1. public class HeaderParser {

  2.    public static final byte[] MAGIC_HEADER = new byte[] { (byte) 0xfe,

  3.            (byte) 0x62, (byte) 0x69, (byte) 0x6e };

  4.    public static void main(String[] args) throws Exception {

  5.        String filePath = "D:\\mysql-bin.000001";

  6.        File binlogFile = new File(filePath);

  7.        ByteArrayInputStream inputStream = null;

  8.        inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));

  9.        byte[] magicHeader = inputStream.read(4);

  10.        if (!Arrays.equals(MAGIC_HEADER, magicHeader)) {

  11.            throw new RuntimeException("binlog文件格式不对");

  12.        }

  13.        EventHeader eventHeader = new EventHeader();

  14.        eventHeader.setTimestamp(inputStream.readLong(4) * 1000L);

  15.        eventHeader.setEventType(inputStream.readInteger(1));

  16.        eventHeader.setServerId(inputStream.readLong(4));

  17.        eventHeader.setEventLength(inputStream.readLong(4));

  18.        eventHeader.setNextPosition(inputStream.readLong(4));

  19.        eventHeader.setFlags(inputStream.readInteger(2));

  20.        System.out.println(eventHeader);

  21.        inputStream.enterBlock((int) (eventHeader.getEventLength() - 19 - 4));

  22.        FormatDescriptionEventData descriptionEventData = new FormatDescriptionEventData();

  23.        descriptionEventData.setBinlogVersion(inputStream.readInteger(2));

  24.        descriptionEventData.setServerVersion(inputStream.readString(50).trim());

  25.        descriptionEventData.setTimestamp(inputStream.readLong(4) * 1000L);

  26.        descriptionEventData.setHeaderLength(inputStream.readInteger(1));

  27.        int sums = inputStream.available();

  28.        for (int i = 0; i < sums; i++) {

  29.            descriptionEventData.getHeaderArrays().add(inputStream.readInteger(1));

  30.        }

  31.        System.out.println(descriptionEventData);

  32.    }

  33. }

至于输出,就不给大家看了,没啥意思。大家看 headerArrays的值即可,如下所示

  1. headerArrays=[56, 13, 0, 8, 0, 18,

  2. 0, 4, 4, 4, 4, 18, 0, 0, 95, 0,

  3. 4, 26, 8, 0, 0, 0, 8, 8, 8, 2, 0, 0,

  4. 0, 10, 10, 10, 42, 42, 0, 18, 52, 0,

    1]

其实他所输出的值,可以在地址 

https://dev.mysql.com/doc/internals/en/format-description-event.html

查询到,该页有一个表格如下所示,其中我红圈的地方,就是私有事件头的长度,即

总结

关于其他事件的结构体,大家可以自行去网站查询,解析原理都是一样的。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存