【原創】研發應該懂的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));
	}
}

輸出如下

魔數\xfe\x62\x69\x6e是否正確:true

在文件頭之后,跟隨的是一個一個事件依次排列。在《binlog二進制文件解析》一文中,將其分為三個部分:通用事件頭(common-header)、私有事件頭(post-header)和事件體(event-body)。本文修改了一下,只用兩個Java類來修飾binlog中的事件,即EventHeaderEventData。可以理解為下述的對應關系:

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

於是,你們可以把Binlog的文件結構像下面這么理解
image
說一下這個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個字節的事件頭長度。得到如下算式

\[119-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
查詢到,該頁有一個表格如下所示,其中我紅圈的地方,就是私有事件頭的長度,即
image

總結

關於其他事件的結構體,大家可以自行去網站查詢,解析原理都是一樣的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM