drc-mysql是一種支持多master 多slave的快速並行復制的解決方案,基於mysql的binlog,目前支持binlog的STATEMENT模式。為了實現drc-mysql對 ROW模式的支持,本文對此展開研究,分析了binlog的事件格式,並針對不同的數據類型進行解析。
本文的目的是為了展示如何從row模式事件中解析數據,因此事件中一些記錄其他信息的字節會直接略過,感興趣的同學可以看看log_event.h以及log_event.cc兩個文件。
獲取Binlog事件:
Mysql對Binlog的處理是以事件為單位的,每一次DML操作可能會產生多次事件,例如對於innodb存儲引擎,會額外產生一條QUERY_EVENT(事務的begin語句)以及XID_EVENT(事務提交)。
通過調用libmysql.so庫中的cli_safe_read()函數可以獲取一次binlog事件:
cli_safe_read(mm); // mm類型為MYSQL*
net = &mm->net;
buf = (const char*) net->read_pos + 1;
Binlog的事件類型大約有27種,這里只介紹與ROW模式相關的事件
1) QUERY_EVENT:與STATEMENT模式處理相同,存儲的是SQL,主要是一些與數據無關的操作,eg: begin、drop table;
2) TABLE_MAP_EVENT:記錄了下一條事件所對應的表信息,在其中存儲了數據庫名和表名;
3) WRITE_ROWS_EVENT:操作類型為insert;
4) UPDATE_ROWS_EVENT:操作類型為update;
5) DELETE_ROWS_EVENT:操作類型為delete;
6) XID_EVENT, 用於標識事務提交。
在buf[EVENT_TYPE_OFFSET]中記錄了事件的類型 (EVENT_TYPE_OFFSET = 4),根據其中記錄的整數,對比log_event.h中的Log_event_type,可以找到相應的事件類型。
以一條insert語句為例,包含4個事件:
TABLE_MAP_EVENT
QUERY_EVENT (begin)
WRITE_ROWS_EVENT
XID_EVENT
事件時間戳:
buf[0] ~ buf[3]的四個字節,存儲了執行操作前的時間戳。
事件長度:
Buf[9]開始的四個字節構成的整數,可以使用如下的方式來進行整數轉換:
#define UCHAR(ptr) ((*(ptr)+256)%256)
const char *ptr = buf + 9;
unsigned int data_len = UCHAR(ptr) + (UCHAR(ptr+1)<<8) + (UCHAR(ptr+2)<<16) + (UCHAR(ptr+3)<<24);
獲取數據庫和表名:
由於在insert/delete/update事件中不記錄表的相關信息,因此每次DML操作都會產生一個TABLE_MAP_EVENT事件,其中存儲了獲取數據庫名和表名。
例如對於數據庫名:tt0001;表名:x18,從buf[27]開始表示為如下格式:
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
6 | t | t | 0 | 0 | 0 | 1 | \0 | 3 | x | 1 | 8 | \0 |
解析數據:
在buf中記錄了很多信息,但我們的目的是為了解析出數據,因此可以跳過一些字節,直接到達我們的目標數據頭部。
cols = buf[27]; //在insert/delete/update事件中,buf[27]表示列的個數
bits = (cols+7)/8
對於WRITE_ROWS_EVENT、DELETE_ROWS_EVENT: ptr = buf +28+bits
對於UPDATE_ROWS_EVENT:ptr = buf +28+bits * 2
從ptr開始,記錄了我們需要解析的數據。
1) UPDATE_ROWS_EVENT
Old record | New record | Old record | New record | Old record | …… |
每更新了多少行,就有多少對 old/new record,當一個事件包存儲不下所有記錄時,將會拆分成多個 UPDATE_ROWS_EVENT事件。
2) WRITE_ROWS_EVENT
包含一條插入的數據record
3) DELETE_ROWS_EVENT
包含被刪除的數據record,格式為:
record | record | record | …… |
從上面的分析可以看出,要想從binlog中解析出數據,除了輔助信息外,關鍵是要從record中獲取得到行數據,因為DML操作對應的事件類型,都以record為記錄單位。
Record結構:
在Record的前幾位,會用多個字節來表示值為NULL的列,record的結構可表示為
bit_map | Col1 | Col2 | Col3 | …… |
其中bit_map的所占字節數為(cols+7)/8
例如,執行:insert into xx values(1, NULL, NULL, 15, “ssss”);
xx表有5列,需要(cols +7)/8 = 1 個字節就可以表示所有的列
bit_map = 230,230轉換為二進制:1110 0110
其中,最低位表示第一列,第cols( = 5)位為最后一列,為1表示該列值為NULL,為0表示非NULL,在隨后的數據記錄中只會記錄非NULL的值,例如這里跳過bit_map所占字節之后,只會記錄1、15和‘ssss’
再比如,當執行如下語句時:insert into x14(a,b) values (NULL,”dsda”);
這里有9列,因此需要2個字節記:
ptr[0] = -4; ptr[1] = -1
轉換為二進制 (ptr[1])1111 1111 (ptr[0])1111 1100
注意,這里雖然在SQL語句中a值為NULL,但由於a列是自增類型,因此存儲在binlog中的就是一個整數,而非NULL值.
對於值為NULL的列,我們可以通過表的定義得到該列的默認值。
解析不同的數據類型:
在record中bit_map之后的列數據中,針對不同的數據類型,可能在record中占用不同的字節,因此需要針對每種數據類型進行處理,為 了獲取到每一列的信息,我們可以調用MYSQL的接口函數mysql_fetch_field()。這里需要注意一種特殊情形,即對於set和enum類 型,在調用該API時,會被轉換為MYSQL_TYPE_STRING類型,可以調用show columns from 來得到這兩種類型的定義。
這里列出了大部分常用數據類型的字節數和解析方法:
1. MYSQL_TYPE_LONG
Int類型,占用4個字節,sint4korr(ptr)
2. MYSQL_TYPE_TINY
Tinyint類型 ,占用1個字節
3. MYSQL_TYPE_SHORT
smallint 類型, 2個字節, sint2korr(ptr)
4. MYSQL_TYPE_INT24
MEDIUMINT類型,3個字節, sint3korr(ptr)
5. MYSQL_TYPE_LONGLONG
Bigint 類型,8個字節, sint8korr(ptr)
6. MYSQL_TYPE_NEWDECIMAL
Decimal類型,精度限制為65, 字節數與該類型的定義相關,可以參考用戶手冊 ,對該類型的解析主要是計算出其占用的字節數,調用libmysql.so庫中的bin2decimal函數來實現解析。
7. MYSQL_TYPE_FLOAT、MYSQL_TYPE_DOUBLE
直接進行類型的強制轉換,分別占4和8個字節,然后根據定義對輸出進行精度控制。
8. MYSQL_TYPE_BIT
Bit類型,占用的字節數與其定義相關,計算方式:
byt_len = length%8==0? length/8 : (length/8 + 1);
例如,當定義為bit(M)時,length = M;將byt_len個字節中存儲的數據轉換為一個整數。
9. MYSQL_TYPE_SET
SET類型,定義為SET(M),M值為以下范圍時:
1 ~8,1個字節
9~16, 2個字節
17~24, 3個字節
25~32, 4個字節
33~64, 8個字節
然后將相應字節內的數轉換為整數即可
10. MYSQL_TYPE_ENUM
Enum類型,當該類型內的元素超過255個時,使用2個字節,否則使用1個字節表示,相應字節內轉換為整數M,表示在enum中的第M個元素。
11. MYSQL_TYPE_STRING、MYSQL_TYPE_VAR_STRING、MYSQL_TYPE_BLOB
包括char()、varchar()以及text類型,其處理方式相同,在record中首先根據其定義的長度,例如:
對於varchar(10),使用一個字節記錄長度;而對於varchar(300),則需要使用兩個字節來記錄字符串的長度;
字符串“abcdef”,在record中被記錄為“6abcdef”。
12. MYSQL_TYPE_TIME
Time類型,3個字節,計算方法:
d_int = UCHAR(ptr) + (UCHAR(ptr+1)<<8) + (UCHAR(ptr+2)<<16);
例如對於‘12:01:22’,計算結果為120122
13. MYSQL_TYPE_TIMESTAMP
timestamp時間戳類型,4個字節,直接進行類型的強制轉換為整數
14. MYSQL_TYPE_DATE
Date類型,3個字節,計算方法:
d_int = UCHAR(ptr) + (UCHAR(ptr+1)<<8) + (UCHAR(ptr+2)<<16)
例如:
00001111 10110111 00100001
其中,1-5位表示日期,6-9位表示月份,剩余的表示年份,因此上述date類型可轉換為2011-09-01
15. MYSQL_TYPE_YEAR
Year類型,1個字節,記錄年份,用一個字節記錄,從1900年開始
例如,當值為112時,表示112+1900 = 2012年
16. MYSQL_TYPE_DATETIME
Datetime類型,8個字節,直接類型轉換為long long,
例如對於 2011-08-27 19:32:46
計算結果值為20110827193246
其實,不管是什么數據類型,我們只要知道其占有的字節數,就能推敲出他們在文件中存儲的格式。
未來可能的應用
1. 通過解析binlog中的行數據,進行增量數據dump;
2. 結合handlersocket進行replication。
http://www.taobaodba.com/html/585_mysql-binlog%E7%9A%84row%E6%A8%A1%E5%BC%8F%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%90.html