MySQL基於ROW格式的數據恢復

   你們都知道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;
View Code

這腳本含有註釋以及使用說明,因此使用起來仍是比較簡單的,若是你會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格式要爲ROWless

<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/*!*/;
View Code

從上面的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

您的支持是對博主最大的鼓勵,感謝您的認真閱讀。本文版權歸做者全部,歡迎轉載,但請保留該聲明。

相關文章
相關標籤/搜索