• 【转载】研发应该懂的binlog知识(下)


    引言

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

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

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

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

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

    正文

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

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

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

    public class MagicParser {
        
        public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};
        
        public static void main(String[] args)throws Exception {
            String filePath = "D:\mysql-bin.000001";
            File binlogFile = new File(filePath);
            ByteArrayInputStream inputStream = null;
            inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));
            byte[] magicHeader = inputStream.read(4);
            System.out.println("魔数\xfe\x62\x69\x6e是否正确:"+Arrays.equals(MAGIC_HEADER, magicHeader));
        }
    }

    输出如下

    魔数xfex62x69x6e是否正确:true

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

    EventHeader --> 通用事件头(common-header)
    EventData ---> 私有事件头(post-header)和事件体(event-body)

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

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

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

    属性字节数含义
    timestamp 4 包含了该事件的开始执行时间
    eventType 1 事件类型
    serverId 4 标识产生该事件的MySQL服务器的server-id
    eventLength 4 该事件的长度(Header+Data+CheckSum)
    nextPosition 4 下一个事件在binlog文件中的位置
    flags 2 标识产生该事件的MySQL服务器的server-id。

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

    public class EventHeader {
        private long timestamp;
        private int eventType;
        private long serverId;
        private long eventLength;
        private long nextPosition;
        private int flags;
        //省略setter和getter方法
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("EventHeader");
            sb.append("{timestamp=").append(timestamp);
            sb.append(", eventType=").append(eventType);
            sb.append(", serverId=").append(serverId);
            sb.append(", eventLength=").append(eventLength);
            sb.append(", nextPosition=").append(nextPosition);
            sb.append(", flags=").append(flags);
            sb.append('}');
            return sb.toString();
        }
    }

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

    public class HeaderParser {
        
        public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};
        
        public static void main(String[] args)throws Exception {
            String filePath = "D:\mysql-bin.000001";
            File binlogFile = new File(filePath);
            ByteArrayInputStream inputStream = null;
            inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));
            byte[] magicHeader = inputStream.read(4);
            if(!Arrays.equals(MAGIC_HEADER, magicHeader)){
                throw new RuntimeException("binlog文件格式不对");
            }
            EventHeader eventHeader = new EventHeader();
            eventHeader.setTimestamp(inputStream.readLong(4) * 1000L);
            eventHeader.setEventType(inputStream.readInteger(1));
            eventHeader.setServerId(inputStream.readLong(4));
            eventHeader.setEventLength(inputStream.readLong(4));
            eventHeader.setNextPosition(inputStream.readLong(4));
            eventHeader.setFlags(inputStream.readInteger(2));       
            System.out.println(eventHeader);
            
        }
    }

    输出如下

    EventHeader{timestamp=1536487335000, eventType=15, serverId=1, eventLength=119, nextPosition=123, flags=1}

    注意看,两个参数

    eventLength=119
    nextPosition=123

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

    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的结构对应如下表所示

    属性字节数含义
    binlogVersion 2 binlog版本
    serverVersion 50 服务器版本
    timestamp 4 该字段指明该binlog文件的创建时间。
    headerLength 1 事件头长度,为19
    headerArrays n 一个数组,标识所有事件的私有事件头的长度
    
    

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

    11919425041=39119−19−4−2−50−4−1=39


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

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

    public class FormatDescriptionEventData {
        private int binlogVersion;
        private String serverVersion;
        private long timestamp;
        private int headerLength;
        private List headerArrays = new ArrayList<Integer>();
        //省略setter和getter方法
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("FormatDescriptionEventData");
            sb.append("{binlogVersion=").append(binlogVersion);
            sb.append(", serverVersion=").append(serverVersion);
            sb.append(", timestamp=").append(timestamp);
            sb.append(", headerLength=").append(headerLength);
            sb.append(", headerArrays=").append(headerArrays);
            sb.append('}');
            return sb.toString();
        }   
    }

    那如何解析呢,如下所示

    public class HeaderParser {
    
        public static final byte[] MAGIC_HEADER = new byte[] { (byte) 0xfe,
                (byte) 0x62, (byte) 0x69, (byte) 0x6e };
    
        public static void main(String[] args) throws Exception {
            String filePath = "D:\mysql-bin.000001";
            File binlogFile = new File(filePath);
            ByteArrayInputStream inputStream = null;
            inputStream = new ByteArrayInputStream(new FileInputStream(binlogFile));
            byte[] magicHeader = inputStream.read(4);
            if (!Arrays.equals(MAGIC_HEADER, magicHeader)) {
                throw new RuntimeException("binlog文件格式不对");
            }
            EventHeader eventHeader = new EventHeader();
            eventHeader.setTimestamp(inputStream.readLong(4) * 1000L);
            eventHeader.setEventType(inputStream.readInteger(1));
            eventHeader.setServerId(inputStream.readLong(4));
            eventHeader.setEventLength(inputStream.readLong(4));
            eventHeader.setNextPosition(inputStream.readLong(4));
            eventHeader.setFlags(inputStream.readInteger(2));
            System.out.println(eventHeader);
            inputStream.enterBlock((int) (eventHeader.getEventLength() - 19 - 4));
            FormatDescriptionEventData descriptionEventData = new FormatDescriptionEventData();
            descriptionEventData.setBinlogVersion(inputStream.readInteger(2));
            descriptionEventData.setServerVersion(inputStream.readString(50).trim());
            descriptionEventData.setTimestamp(inputStream.readLong(4) * 1000L);
            descriptionEventData.setHeaderLength(inputStream.readInteger(1));
            int sums = inputStream.available();
            for (int i = 0; i < sums; i++) {
                descriptionEventData.getHeaderArrays().add(inputStream.readInteger(1));
            }
            System.out.println(descriptionEventData);
        }
    }

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

    headerArrays=[56, 13, 0, 8, 0, 18, 0, 4, 4, 4, 4, 18, 0, 0, 95, 0, 4, 26, 8, 0, 0, 0, 8, 8, 8, 2, 0, 0, 0, 10, 10, 10, 42, 42, 0, 18, 52, 0, 1]

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

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

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

    总结

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

    作者:孤独烟 出处: http://rjzheng.cnblogs.com/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。
    
    
    
    
    
    
    
    
    
    
    
    
    
    
  • 相关阅读:
    第七周进度报告
    解析极限编程阅读笔记02
    解析极限编程阅读笔记01
    第六周进度报告
    合作开发项目-地铁线路查询
    构建之法阅读笔记03
    关于DeferredResult的思考
    java实现任务调度
    java里面如何提升编写速度
    收集整理mysql数据库设计规范与原则
  • 原文地址:https://www.cnblogs.com/nov5026/p/12058405.html
Copyright © 2020-2023  润新知