你們都知道MySQL Binlog 有三種格式,分別是Statement、Row、Mixd。Statement記錄了用戶執行的原始SQL,而Row則是記錄了行的修改狀況,在MySQL 5.6以上的版本默認是Mixd格式,但爲了保證複製數據的完整性,建議生產環境都使用Row格式,就前面所說的Row記錄的是行數據的修改狀況,而不是原始SQL。那麼線上或者測試環境誤操刪除或者更新幾條數據後,又想恢復,那怎麼辦呢?下面演示基於Binlog格式爲Row的誤操後數據恢復,那麼怎麼把Binlog解析出來生成反向的原始SQL呢?下面咱們一塊兒來學習。mysql
下面咱們使用 binlog-rollback.pl 對數據進行恢復演示。(這腳本的做者不知道是誰,Github上也沒找到這個腳本,因此沒法標明出處),腳本是用Perl語言寫的,很是好用的一個腳本,固然你也能夠用Shell或者Python腳原本實現,下面是腳本的代碼:sql
#!/usr/lib/perl -w use strict; use warnings; use Class::Struct; use Getopt::Long qw(:config no_ignore_case); # GetOption # register handler system signals use sigtrap 'handler', \&sig_int, 'normal-signals'; # catch signal sub sig_int(){ my ($signals) = @_; print STDERR "# Caught SIG$signals.\n"; exit 1; } my %opt; my $srcfile; my $host = '127.0.0.1'; my $port = 3306; my ($user,$pwd); my ($MYSQL, $MYSQLBINLOG, $ROLLBACK_DML); my $outfile = '/dev/null'; my (%do_dbs,%do_tbs); # tbname=>tbcol, tbcol: @n=>colname,type my %tbcol_pos; my $SPLITER_COL = ','; my $SQLTYPE_IST = 'INSERT'; my $SQLTYPE_UPD = 'UPDATE'; my $SQLTYPE_DEL = 'DELETE'; my $SQLAREA_WHERE = 'WHERE'; my $SQLAREA_SET = 'SET'; my $PRE_FUNCT = '========================== '; # ========================================================= # 基於row模式的binlog,生成DML(insert/update/delete)的rollback語句 # 經過mysqlbinlog -v 解析binlog生成可讀的sql文件 # 提取須要處理的有效sql # "### "開頭的行.若是輸入的start-position位於某個event group中間,則會致使"沒法識別event"錯誤 # # 將INSERT/UPDATE/DELETE 的sql反轉,而且1個完整sql只能佔1行 # INSERT: INSERT INTO => DELETE FROM, SET => WHERE # UPDATE: WHERE => SET, SET => WHERE # DELETE: DELETE FROM => INSERT INTO, WHERE => SET # 用列名替換位置@{1,2,3} # 經過desc table得到列順序及對應的列名 # 特殊列類型value作特別處理 # 逆序 # # 注意: # 表結構與如今的表結構必須相同[謹記] # 因爲row模式是冪等的,而且恢復是一次性,因此只提取sql,不提取BEGIN/COMMIT # 只能對INSERT/UPDATE/DELETE進行處理 # ======================================================== sub main{ # get input option &get_options(); # &init_tbcol(); # &do_binlog_rollback(); } &main(); # ---------------------------------------------------------------------------------------- # Func : get options and set option flag # ---------------------------------------------------------------------------------------- sub get_options{ #Get options info GetOptions(\%opt, 'help', # OUT : print help info 'f|srcfile=s', # IN : binlog file 'o|outfile=s', # out : output sql file 'h|host=s', # IN : host 'u|user=s', # IN : user 'p|password=s', # IN : password 'P|port=i', # IN : port 'start-datetime=s', # IN : start datetime 'stop-datetime=s', # IN : stop datetime 'start-position=i', # IN : start position 'stop-position=i', # IN : stop position 'd|database=s', # IN : database, split comma 'T|table=s', # IN : table, split comma 'i|ignore', # IN : ignore binlog check ddl and so on 'debug', # IN : print debug information ) or print_usage(); if (!scalar(%opt)) { &print_usage(); } # Handle for options if ($opt{'f'}){ $srcfile = $opt{'f'}; }else{ &merror("please input binlog file"); } $opt{'h'} and $host = $opt{'h'}; $opt{'u'} and $user = $opt{'u'}; $opt{'p'} and $pwd = $opt{'p'}; $opt{'P'} and $port = $opt{'P'}; if ($opt{'o'}) { $outfile = $opt{'o'}; # 清空 outfile `echo '' > $outfile`; } # $MYSQL = qq{mysql -h$host -u$user -p'$pwd' -P$port}; &mdebug("get_options::MYSQL\n\t$MYSQL"); # 提取binlog,不須要顯示列定義信息,用-v,而不用-vv $MYSQLBINLOG = qq{mysqlbinlog -v}; $MYSQLBINLOG .= " --start-position=".$opt{'start-position'} if $opt{'start-position'}; $MYSQLBINLOG .= " --stop-position=".$opt{'stop-position'} if $opt{'stop-postion'}; $MYSQLBINLOG .= " --start-datetime='".$opt{'start-datetime'}."'" if $opt{'start-datetime'}; $MYSQLBINLOG .= " --stop-datetime='$opt{'stop-datetime'}'" if $opt{'stop-datetime'}; $MYSQLBINLOG .= " $srcfile"; &mdebug("get_options::MYSQLBINLOG\n\t$MYSQLBINLOG"); # 檢查binlog中是否含有 ddl sql: CREATE|ALTER|DROP|RENAME &check_binlog() unless ($opt{'i'}); # 不使用mysqlbinlog過濾,USE dbname;方式可能會漏掉某些sql,因此不在mysqlbinlog過濾 # 指定數據庫 if ($opt{'d'}){ my @dbs = split(/,/,$opt{'d'}); foreach my $db (@dbs){ $do_dbs{$db}=1; } } # 指定表 if ($opt{'T'}){ my @tbs = split(/,/,$opt{'T'}); foreach my $tb (@tbs){ $do_tbs{$tb}=1; } } # 提取有效DML SQL $ROLLBACK_DML = $MYSQLBINLOG." | grep '^### '"; # 去掉註釋: '### ' -> '' # 刪除首尾空格 $ROLLBACK_DML .= " | sed 's/###\\s*//g;s/\\s*\$//g'"; &mdebug("rollback dml\n\t$ROLLBACK_DML"); # 檢查內容是否爲空 my $cmd = "$ROLLBACK_DML | wc -l"; &mdebug("check contain dml sql\n\t$cmd"); my $size = `$cmd`; chomp($size); unless ($size >0){ &merror("binlog DML is empty:$ROLLBACK_DML"); }; } # ---------------------------------------------------------------------------------------- # Func : check binlog contain DDL # ---------------------------------------------------------------------------------------- sub check_binlog{ &mdebug("$PRE_FUNCT check_binlog"); my $cmd = "$MYSQLBINLOG "; $cmd .= " | grep -E -i '^(CREATE|ALTER|DROP|RENAME)' "; &mdebug("check binlog has DDL cmd\n\t$cmd"); my $ddlcnt = `$cmd`; chomp($ddlcnt); my $ddlnum = `$cmd | wc -l`; chomp($ddlnum); my $res = 0; if ($ddlnum>0){ # 在ddl sql前面加上前綴<DDL> $ddlcnt = `echo '$ddlcnt' | sed 's/^/<DDL>/g'`; &merror("binlog contain $ddlnum DDL:$MYSQLBINLOG. ddl sql:\n$ddlcnt"); } return $res; } # ---------------------------------------------------------------------------------------- # Func : init all table column order # if input --database --table params, only get set table column order # ---------------------------------------------------------------------------------------- sub init_tbcol{ &mdebug("$PRE_FUNCT init_tbcol"); # 提取DML語句 my $cmd .= "$ROLLBACK_DML | grep -E '^(INSERT|UPDATE|DELETE)'"; # 提取表名,並去重 #$cmd .= " | awk '{if (\$1 ~ \"^UPDATE\") {print \$2}else {print \$3}}' | uniq "; $cmd .= " | awk '{if (\$1 ~ \"^UPDATE\") {print \$2}else {print \$3}}' | sort | uniq "; &mdebug("get table name cmd\n\t$cmd"); open ALLTABLE, "$cmd | " or die "can't open file:$cmd\n"; while (my $tbname = <ALLTABLE>){ chomp($tbname); #if (exists $tbcol_pos{$tbname}){ # next; #} &init_one_tbcol($tbname) unless (&ignore_tb($tbname)); } close ALLTABLE or die "can't close file:$cmd\n"; # init tb col foreach my $tb (keys %tbcol_pos){ &mdebug("tbname->$tb"); my %colpos = %{$tbcol_pos{$tb}}; foreach my $pos (keys %colpos){ my $col = $colpos{$pos}; my ($cname,$ctype) = split(/$SPLITER_COL/, $col); &mdebug("\tpos->$pos,cname->$cname,ctype->$ctype"); } } }; # ---------------------------------------------------------------------------------------- # Func : init one table column order # ---------------------------------------------------------------------------------------- sub init_one_tbcol{ my $tbname = shift; &mdebug("$PRE_FUNCT init_one_tbcol"); # 獲取表結構及列順序 my $cmd = $MYSQL." --skip-column-names --silent -e 'desc $tbname'"; # 提取列名,並拼接 $cmd .= " | awk -F\'\\t\' \'{print NR\"$SPLITER_COL`\"\$1\"`$SPLITER_COL\"\$2}'"; &mdebug("get table column infor cmd\n\t$cmd"); open TBCOL,"$cmd | " or die "can't open desc $tbname;"; my %colpos; while (my $line = <TBCOL>){ chomp($line); my ($pos,$col,$coltype) = split(/$SPLITER_COL/,$line); &mdebug("linesss=$line\n\t\tpos=$pos\n\t\tcol=$col\n\t\ttype=$coltype"); $colpos{$pos} = $col.$SPLITER_COL.$coltype; } close TBCOL or die "can't colse desc $tbname"; $tbcol_pos{$tbname} = \%colpos; } # ---------------------------------------------------------------------------------------- # Func : rollback sql: INSERT/UPDATE/DELETE # ---------------------------------------------------------------------------------------- sub do_binlog_rollback{ my $binlogfile = "$ROLLBACK_DML "; &mdebug("$PRE_FUNCT do_binlog_rollback"); # INSERT|UPDATE|DELETE my $sqltype; # WHERE|SET my $sqlarea; my ($tbname, $sqlstr) = ('', ''); my ($notignore, $isareabegin) = (0,0); # output sql file open SQLFILE, ">> $outfile" or die "Can't open sql file:$outfile"; # binlog file open BINLOG, "$binlogfile |" or die "Can't open file: $binlogfile"; while (my $line = <BINLOG>){ chomp($line); if ($line =~ /^(INSERT|UPDATE|DELETE)/){ # export sql if ($sqlstr ne ''){ $sqlstr .= ";\n"; print SQLFILE $sqlstr; &mdebug("export sql\n\t".$sqlstr); $sqlstr = ''; } if ($line =~ /^INSERT/){ $sqltype = $SQLTYPE_IST; $tbname = `echo '$line' | awk '{print \$3}'`; chomp($tbname); $sqlstr = qq{DELETE FROM $tbname}; }elsif ($line =~ /^UPDATE/){ $sqltype = $SQLTYPE_UPD; $tbname = `echo '$line' | awk '{print \$2}'`; chomp($tbname); $sqlstr = qq{UPDATE $tbname}; }elsif ($line =~ /^DELETE/){ $sqltype = $SQLTYPE_DEL; $tbname = `echo '$line' | awk '{print \$3}'`; chomp($tbname); $sqlstr = qq{INSERT INTO $tbname}; } # check ignore table if(&ignore_tb($tbname)){ $notignore = 0; &mdebug("<BINLOG>#IGNORE#:line:".$line); $sqlstr = ''; }else{ $notignore = 1; &mdebug("<BINLOG>#DO#:line:".$line); } }else { if($notignore){ &merror("can't get tbname") unless (defined($tbname)); if ($line =~ /^WHERE/){ $sqlarea = $SQLAREA_WHERE; $sqlstr .= qq{ SET}; $isareabegin = 1; }elsif ($line =~ /^SET/){ $sqlarea = $SQLAREA_SET; $sqlstr .= qq{ WHERE}; $isareabegin = 1; }elsif ($line =~ /^\@/){ $sqlstr .= &deal_col_value($tbname, $sqltype, $sqlarea, $isareabegin, $line); $isareabegin = 0; }else{ &mdebug("::unknown sql:".$line); } } } } # export last sql if ($sqlstr ne ''){ $sqlstr .= ";\n"; print SQLFILE $sqlstr; &mdebug("export sql\n\t".$sqlstr); } close BINLOG or die "Can't close binlog file: $binlogfile"; close SQLFILE or die "Can't close out sql file: $outfile"; # 逆序 # 1!G: 只有第一行不執行G, 將hold space中的內容append回到pattern space # h: 將pattern space 拷貝到hold space # $!d: 除最後一行都刪除 my $invert = "sed -i '1!G;h;\$!d' $outfile"; my $res = `$invert`; &mdebug("inverter order sqlfile :$invert"); } # ---------------------------------------------------------------------------------------- # Func : transfer column pos to name # deal column value # # &deal_col_value($tbname, $sqltype, $sqlarea, $isareabegin, $line); # ---------------------------------------------------------------------------------------- sub deal_col_value($$$$$){ my ($tbname, $sqltype, $sqlarea, $isareabegin, $line) = @_; &mdebug("$PRE_FUNCT deal_col_value"); &mdebug("input:tbname->$tbname,type->$sqltype,area->$sqlarea,areabegin->$isareabegin,line->$line"); my @vals = split(/=/, $line); my $pos = substr($vals[0],1); my $valstartpos = length($pos)+2; my $val = substr($line,$valstartpos); my %tbcol = %{$tbcol_pos{$tbname}}; my ($cname,$ctype) = split(/$SPLITER_COL/,$tbcol{$pos}); &merror("can't get $tbname column $cname type") unless (defined($cname) || defined($ctype)); &mdebug("column infor:cname->$cname,type->$ctype"); # join str my $joinstr; if ($isareabegin){ $joinstr = ' '; }else{ # WHERE 被替換爲 SET, 使用 , 鏈接 if ($sqlarea eq $SQLAREA_WHERE){ $joinstr = ', '; # SET 被替換爲 WHERE 使用 AND 鏈接 }elsif ($sqlarea eq $SQLAREA_SET){ $joinstr = ' AND '; }else{ &merror("!!!!!!The scripts error"); } } # my $newline = $joinstr; # NULL value if (($val eq 'NULL') && ($sqlarea eq $SQLAREA_SET)){ $newline .= qq{ $cname IS NULL}; }else{ # timestamp: record seconds if ($ctype eq 'timestamp'){ $newline .= qq{$cname=from_unixtime($val)}; # datetime: @n=yyyy-mm-dd hh::ii::ss }elsif ($ctype eq 'datetime'){ $newline .= qq{$cname='$val'}; }else{ $newline .= qq{$cname=$val}; } } &mdebug("\told>$line\n\tnew>$newline"); return $newline; } # ---------------------------------------------------------------------------------------- # Func : check is ignore table # params: IN table full name # format:`dbname`.`tbname` # RETURN: # 0 not ignore # 1 ignore # ---------------------------------------------------------------------------------------- sub ignore_tb($){ my $fullname = shift; # 刪除` $fullname =~ s/`//g; my ($dbname,$tbname) = split(/\./,$fullname); my $res = 0; # 指定了數據庫 if ($opt{'d'}){ # 與指定庫相同 if ($do_dbs{$dbname}){ # 指定表 if ($opt{'T'}){ # 與指定表不一樣 unless ($do_tbs{$tbname}){ $res = 1; } } # 與指定庫不一樣 }else{ $res = 1; } } #&mdebug("Table check ignore:$fullname->$res"); return $res; } # ---------------------------------------------------------------------------------------- # Func : print debug msg # ---------------------------------------------------------------------------------------- sub mdebug{ my (@msg) = @_; print "<DEBUG>@msg\n" if ($opt{'debug'}); } # ---------------------------------------------------------------------------------------- # Func : print error msg and exit # ---------------------------------------------------------------------------------------- sub merror{ my (@msg) = @_; print "<Error>:@msg\n"; &print_usage(); exit(1); } # ---------------------------------------------------------------------------------------- # Func : print usage # ---------------------------------------------------------------------------------------- sub print_usage{ print <<EOF; ========================================================================================== Command line options : --help # OUT : print help info -f, --srcfile # IN : binlog file. [required] -o, --outfile # OUT : output sql file. [required] -h, --host # IN : host. default '127.0.0.1' -u, --user # IN : user. [required] -p, --password # IN : password. [required] -P, --port # IN : port. default '3306' --start-datetime # IN : start datetime --stop-datetime # IN : stop datetime --start-position # IN : start position --stop-position # IN : stop position -d, --database # IN : database, split comma -T, --table # IN : table, split comma. [required] set -d -i, --ignore # IN : ignore binlog check contain DDL(CREATE|ALTER|DROP|RENAME) --debug # IN : print debug information Sample : shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' -i shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' --debug shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -h '192.168.1.2' -u 'user' -p 'pwd' -P 3307 shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' --start-position=107 shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' --start-position=107 --stop-position=10000 shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' -d 'db1,db2' shell> perl binlog-rollback.pl -f 'mysql-bin.0000*' -o '/tmp/t.sql' -u 'user' -p 'pwd' -d 'db1,db2' -T 'tb1,tb2' ========================================================================================== EOF exit; } 1;
這腳本含有註釋以及使用說明,因此使用起來仍是比較簡單的,若是你會Perl語言,相信也很容易看懂代碼。binlog-rollback.pl的使用參數以下:shell
[root@localhost mysql3306]# perl binlog-rollback.pl ========================================================================================== Command line options : --help # OUT : print help info -f, --srcfile # IN : binlog file. [required] -o, --outfile # OUT : output sql file. [required] -h, --host # IN : host. default '127.0.0.1' -u, --user # IN : user. [required] -p, --password # IN : password. [required] -P, --port # IN : port. default '3306' --start-datetime # IN : start datetime --stop-datetime # IN : stop datetime --start-position # IN : start position --stop-position # IN : stop position -d, --database # IN : database, split comma -T, --table # IN : table, split comma. [required] set -d -i, --ignore # IN : ignore binlog check contain DDL(CREATE|ALTER|DROP|RENAME) --debug # IN : print debug information Sample : shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' -i shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' --debug shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -h '192.168.1.2' -u 'user' -p 'pwd' -P 3307 shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' --start-position=107 shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' --start-position=107 --stop-position=10000 shell> perl binlog-rollback.pl -f 'mysql-bin.000001' -o '/tmp/t.sql' -u 'user' -p 'pwd' -d 'db1,db2' shell> perl binlog-rollback.pl -f 'mysql-bin.0000*' -o '/tmp/t.sql' -u 'user' -p 'pwd' -d 'db1,db2' -T 'tb1,tb2' ========================================================================================== [root@localhost mysql3306]#
下面主要演示對一個表的增、刪、修(Insert/Delete/Update)操做,基於Binlog爲Row格式的反向解析。數據庫
細心看腳本的朋友都能看到這個腳本須要提供一個鏈接MySQL的用戶,主要是爲了獲取表結構。下面咱們測試一個普通用戶並給予SELECT權限便可,默認是host是127.0.0.1,這個能夠修改腳本,我這裏按腳本默認的:app
<Test>[(none)]> GRANT SELECT ON *.* TO 'recovery'@'127.0.0.1' identified by '123456'; Query OK, 0 rows affected (0.08 sec) <Test>[(none)]> flush privileges; Query OK, 0 rows affected (0.04 sec) <Test>[(none)]>
往xuanzhi庫的表tb1裏插入2行數據,記得binlog格式要爲ROW:less
<Test>[xuanzhi]> show global variables like 'binlog_format'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | binlog_format | ROW | +---------------+-------+ 1 row in set (0.00 sec) <Test>[xuanzhi]> insert into xuanzhi.tb1 select 1,'aa'; Query OK, 1 row affected (0.01 sec) Records: 1 Duplicates: 0 Warnings: 0 <Test>[xuanzhi]> insert into xuanzhi.tb1 select 2,'cc'; Query OK, 1 row affected (0.01 sec) Records: 1 Duplicates: 0 Warnings: 0 <Test>[xuanzhi]> select * from xuanzhi.tb1; +------+------+ | id | name | +------+------+ | 1 | aa | | 2 | cc | +------+------+ 2 rows in set (0.00 sec) <Test>[xuanzhi]>
爲了看到運行腳本在不指定庫看到的效果,我這裏再往test庫的user表插入兩行數據:運維
<Test>[xuanzhi]> insert into test.user select 1,'user1',20; Query OK, 1 row affected (0.03 sec) Records: 1 Duplicates: 0 Warnings: 0 <Test>[xuanzhi]> insert into test.user select 2,'user2',30; Query OK, 1 row affected (0.01 sec) Records: 1 Duplicates: 0 Warnings: 0 <Test>[xuanzhi]>
查看此時的此時處於那個binlog:ide
<Test>[xuanzhi]> show master status; +----------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +----------------------+----------+--------------+------------------+-------------------+ | localhost-bin.000023 | 936 | | | | +----------------------+----------+--------------+------------------+-------------------+ 1 row in set (0.00 sec) <Test>[xuanzhi]>
下面運行腳本 binlog-rollback.pl ,不指定任何庫和表的狀況下,這時表把binlog裏全部DML操做都生成反向的SQL(最新的DML會生成在輸入文件的最前面):post
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' mysql: [Warning] Using a password on the command line interface can be insecure. mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]#
咱們查看輸出的文件:/data/t.sql學習
[root@localhost mysql3306]# cat /data/t.sql DELETE FROM `test`.`user` WHERE `id`=2 AND `name`='user2' AND `age`=30; DELETE FROM `test`.`user` WHERE `id`=1 AND `name`='user1' AND `age`=20; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=2 AND `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=1 AND `name`='aa';
能夠看到,INSERT操做的反向操做就是DELETE,這裏把全部庫的DML操做都查出來了,在後面會演示找單個庫或者表所產生的反向SQL。
下面模擬運維人員、開發人員或者DBA誤操刪除數據,分別在不一樣的庫刪除一條記錄:
<Test>[xuanzhi]> delete from xuanzhi.tb1 where id=2; Query OK, 1 row affected (0.06 sec) <Test>[xuanzhi]> delete from test.user where id=1; Query OK, 1 row affected (0.00 sec) <Test>[xuanzhi]>
這個時候發現本身刪除錯了,須要恢復,恰好這些數據不在最新的備份裏,正常的恢復方法有兩種:
1、是基於最新的完整備份+binlog進行數據恢復了,這時須要把備份導回去,還要找出Binlog DELETE前的pos位置,再進行binlog恢復,恢復完後再把記錄恢復到誤操的環境上。若是表很大,這時間要好久。
2、由於Binlog格式爲ROW時,記錄了行的修改,因此DELETE是能夠看到全部列的值的,把binlog解析出來,找到被DELETE的記錄,經過各類處理再恢復回去,但binlog不能基於一個庫或表級別的解析,只能整個binlog解析再進行操做。
以上的方法都比較消耗時間,固然使用binlog-rollback.pl腳本有點相似第二種方法,可是binlog-rollback.pl能夠指定庫或表進行反向解析,還能夠指定POS點,效率至關更高一些。
下面咱們運行 binlog-rollback.pl 腳本,生成刪除數據語句的反向SQL:
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' mysql: [Warning] Using a password on the command line interface can be insecure. mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]#
再次查看輸出文件:
[root@localhost mysql3306]# cat /data/t.sql INSERT INTO `test`.`user` SET `id`=1, `name`='user1', `age`=20; INSERT INTO `xuanzhi`.`tb1` SET `id`=2, `name`='bb'; DELETE FROM `test`.`user` WHERE `id`=2 AND `name`='user2' AND `age`=30; DELETE FROM `test`.`user` WHERE `id`=1 AND `name`='user1' AND `age`=20; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=2 AND `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=1 AND `name`='aa'; [root@localhost mysql3306]#
剛剛DELETE的2條記錄已經生成了反向INSERT語句,這樣恢復就簡單多啦:
INSERT INTO `test`.`user` SET `id`=1, `name`='user1', `age`=20; INSERT INTO `xuanzhi`.`tb1` SET `id`=2, `name`='bb';
下面咱們模擬修改數據的時候,誤修改了,以下:
<Test>[xuanzhi]> select * from xuanzhi.tb1; +------+------+ | id | name | +------+------+ | 1 | aa | +------+------+ 1 row in set (0.00 sec) <Test>[xuanzhi]> select * from test.user; +------+-------+------+ | id | name | age | +------+-------+------+ | 2 | user2 | 30 | +------+-------+------+ 1 row in set (0.00 sec) <Test>[xuanzhi]> update xuanzhi.tb1 set name = 'MySQL' where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 <Test>[xuanzhi]> update test.user set age = 20 where id = 2; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 <Test>[xuanzhi]>
這個時候發現修改錯數據了,須要還原,一樣可使用腳本binlog-rollback.pl 進行對所在Binlog的DML生成反向的SQL,進行恢復:
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' mysql: [Warning] Using a password on the command line interface can be insecure. mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]#
再查看輸出文件:
[root@localhost mysql3306]# cat /data/t.sql UPDATE `test`.`user` SET `id`=2, `name`='user2', `age`=30 WHERE `id`=2 AND `name`='user2' AND `age`=20; UPDATE `xuanzhi`.`tb1` SET `id`=1, `name`='aa' WHERE `id`=1 AND `name`='MySQL'; INSERT INTO `test`.`user` SET `id`=1, `name`='user1', `age`=20; INSERT INTO `xuanzhi`.`tb1` SET `id`=2, `name`='bb'; DELETE FROM `test`.`user` WHERE `id`=2 AND `name`='user2' AND `age`=30; DELETE FROM `test`.`user` WHERE `id`=1 AND `name`='user1' AND `age`=20; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=2 AND `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=1 AND `name`='aa'; [root@localhost mysql3306]#
能夠看到生成了反向的UPDATE語句:
UPDATE `test`.`user` SET `id`=2, `name`='user2', `age`=30 WHERE `id`=2 AND `name`='user2' AND `age`=20; UPDATE `xuanzhi`.`tb1` SET `id`=1, `name`='aa' WHERE `id`=1 AND `name`='MySQL';
下面進行指定庫的反向解析,參數爲(-d)
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' -d 'xuanzhi' mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]# cat /data/t.sql UPDATE `xuanzhi`.`tb1` SET `id`=1, `name`='aa' WHERE `id`=1 AND `name`='MySQL'; INSERT INTO `xuanzhi`.`tb1` SET `id`=2, `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=2 AND `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=1 AND `name`='aa'; [root@localhost mysql3306]#
能夠看到輸入的文件只含xuanzhi庫的全部DML的反向SQL。
下面進行指定庫下某個表的反向解析,參數爲:-T (爲了看到效果在xuanzhi庫下的tb2表刪除一些記錄):
<Test>[xuanzhi]> select * from tb2; +------+------+ | id | name | +------+------+ | 1 | aa | | 2 | bb | | 3 | cc | +------+------+ 3 rows in set (0.04 sec) <Test>[xuanzhi]> delete from xuanzhi.tb2 where id <2; Query OK, 1 row affected (0.02 sec) <Test>[xuanzhi]>
這個時候應該若是隻指定xuanzhi庫,那麼tb1和tb2的DML操做的反向操做都會記錄下來:
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' -d 'xuanzhi' mysql: [Warning] Using a password on the command line interface can be insecure. mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]# cat /data/t.sql INSERT INTO `xuanzhi`.`tb2` SET `id`=1, `name`='aa'; UPDATE `xuanzhi`.`tb1` SET `id`=1, `name`='aa' WHERE `id`=1 AND `name`='MySQL'; INSERT INTO `xuanzhi`.`tb1` SET `id`=2, `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=2 AND `name`='bb'; DELETE FROM `xuanzhi`.`tb1` WHERE `id`=1 AND `name`='aa'; [root@localhost mysql3306]#
指定單個表tb2:
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' -d 'xuanzhi' -T 'tb2' mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]# cat /data/t.sql INSERT INTO `xuanzhi`.`tb2` SET `id`=1, `name`='aa'; [root@localhost mysql3306]#
由於上面刪除了一條tb2的數據,全部這個文件就對應生成一條tb2的INSERT記錄
下面進行POS點生成反向SQL:(--start-position= --stop-position=)
# at 1557 #160308 4:27:23 server id 1283306 end_log_pos 1632 CRC32 0xb67ef6ba Query thread_id=11 exec_time=0 error_code=0 SET TIMESTAMP=1457382443/*!*/; BEGIN /*!*/; # at 1632 #160308 4:27:23 server id 1283306 end_log_pos 1683 CRC32 0x219a127c Table_map: `test`.`user` mapped to number 74 # at 1683 #160308 4:27:23 server id 1283306 end_log_pos 1749 CRC32 0xf5e0d39e Update_rows: table id 74 flags: STMT_END_F BINLOG ' K+TdVhPqlBMAMwAAAJMGAAAAAEoAAAAAAAEABHRlc3QABHVzZXIAAwP+AwL+Hgd8Epoh K+TdVh/qlBMAQgAAANUGAAAAAEoAAAAAAAEAAgAD///4AgAAAAV1c2VyMh4AAAD4AgAAAAV1c2Vy MhQAAACe0+D1 '/*!*/; ### UPDATE `test`.`user` ### WHERE ### @1=2 ### @2='user2' ### @3=30 ### SET ### @1=2 ### @2='user2' ### @3=20 # at 1749 #160308 4:27:23 server id 1283306 end_log_pos 1780 CRC32 0x1e62cb77 Xid = 101 COMMIT/*!*/; # at 1780 #160308 4:40:32 server id 1283306 end_log_pos 1855 CRC32 0x04dfe1f0 Query thread_id=11 exec_time=1 error_code=0 SET TIMESTAMP=1457383232/*!*/; BEGIN /*!*/; # at 1855 #160308 4:40:32 server id 1283306 end_log_pos 1907 CRC32 0x897ae6bd Table_map: `xuanzhi`.`tb2` mapped to number 70 # at 1907 #160308 4:40:32 server id 1283306 end_log_pos 1950 CRC32 0xea61aff0 Delete_rows: table id 70 flags: STMT_END_F BINLOG ' QOfdVhPqlBMANAAAAHMHAAAAAEYAAAAAAAEAB3h1YW56aGkAA3RiMgACA/4C/goDveZ6iQ== QOfdViDqlBMAKwAAAJ4HAAAAAEYAAAAAAAEAAgAC//wBAAAAAmFh8K9h6g== '/*!*/; ### DELETE FROM `xuanzhi`.`tb2` ### WHERE ### @1=1 ### @2='aa' # at 1950 #160308 4:40:32 server id 1283306 end_log_pos 1981 CRC32 0x49e1ce9c Xid = 113 COMMIT/*!*/;
從上面的binlog能夠看到開始的--start-position=1557 結束的--stop-position=1981,這一段binlog裏作了UPDATE `test`.`user` ... 和 DELETE FROM `xuanzhi`.`tb2` ... 的操做,那麼用binlog-rollback.pl應該會生成一個UPDATE和一個INSERT語句
[root@localhost mysql3306]# perl binlog-rollback.pl -f 'localhost-bin.000023' -o '/data/t.sql' -u 'recovery' -p '123456' --start-position=1557 --stop-position=1981 mysql: [Warning] Using a password on the command line interface can be insecure. mysql: [Warning] Using a password on the command line interface can be insecure. [root@localhost mysql3306]# cat /data/t.sql INSERT INTO `xuanzhi`.`tb2` SET `id`=1, `name`='aa'; UPDATE `test`.`user` SET `id`=2, `name`='user2', `age`=30 WHERE `id`=2 AND `name`='user2' AND `age`=20; [root@localhost mysql3306]#
更多的測試,就看同窗們了,有測試不當的地方請告訴我,你們一塊兒學習。
總結: 1、感謝那些有分享精神的大神們,讓咱們學到了更多的東西,但開源的腳本須要多測試。
2、誤操的狀況,時有發生,因此咱們要作好備份,作好一些數據恢復的測試。
3、該腳本在處理比較在的binlog時,會常常出現些小問題
做者:陸炫志 出處:xuanzhi的博客 http://www.cnblogs.com/xuanzhi201111 您的支持是對博主最大的鼓勵,感謝您的認真閱讀。本文版權歸做者全部,歡迎轉載,但請保留該聲明。 |