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(" old>$line
new>$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
" if ($opt{"debug"}); } # ---------------------------------------------------------------------------------------- # Func : print error msg and exit # ---------------------------------------------------------------------------------------- sub merror{ my (@msg) = @_; print "<Error>:@msg
"; &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 informationSample : 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的使用参数如下: [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 informationSample : 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,这个可以修改脚本,我这里按脚本默认的:<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:<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:<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会生成在输入文件的最前面):[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]> 这个时候发现自己删除错了,需要恢复,刚好这些数据不在最新的备份里,正常的恢复方法有两种:一、是基于最新的完整备份+binlog进行数据恢复了,这时需要把备份导回去,还要找出Binlog DELETE前的pos位置,再进行binlog恢复,恢复完后再把记录恢复到误操的环境上。如果表很大,这时间要很久。 二、因为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=)从上面的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]#更多的测试,就看同学们了,有测试不当的地方请告诉我,大家一起学习。总结:一、感谢那些有分享精神的大神们,让我们学到了更多的东西,但开源的脚本需要多测试。 二、误操的情况,时有发生,所以我们要做好备份,做好一些数据恢复的测试。本文永久更新链接地址