MySQL基于ROW格式的数据恢复
参考:http://www.cnblogs.com/xuanzhi201111/p/5284084.html
MySQL的bin log日志有三种模式Statement、Row、Mixd,Row记录是行的修改情况建议生产环境使用Row格式,记录的是行数据的修改情况,不是原始SQL,万一误操作删除或者更新后又想恢复,把binlog解析出来生产反向的原始SQL
使用脚本binlog-rollback.pl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 | #!/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 ; |
该脚本只能反向DML语句及对表的增删修改(insert/delete/update)操作
查看日志格式,必须为row格式
1 | show variables like "binlog_format" ; |
show variables like "binlog_format";
运行脚本不加参数会显示脚本帮助
-f自定binlog日志文件
-o输出反向后的sql语句
-u用户名
-p密码
-i忽略DDL语句
-d指定库名
-T指定表名
--start-position --stop-position指定读取的POS开始及结束位置
数据库新建一个库test及库下的表test用于测试
1 2 3 | create database test; use table create table test( id int ) |
查看bin log文件及POS位置值
往表test里面插入一条数据
1 | insert into test values( 1 ); |
反向解析
1 | perl binlog - rollback.pl - f 'mysql-bin.002611' - o '/tmp/t.sql' - u 'root' - p '123456' - i |
查看解析出来的反向语句
1 2 | cat / tmp / t.sql DELETE FROM `test`.`test` WHERE ` id ` = 1 ; |
插入数据的反向是删除,把该sql导入mysql则可以把刚刚的操作取消
模拟误操作把该条数据删除
1 | delete from test.test where id = 1 ; |
查看已经没有该数据了
反向解析,删除的反向就是插入,把该语句导入mysql即可
1 | mysql - uroot - p123456 - h127. 0.0 . 1 < / tmp / t.sql |
查看数据回来了
模拟修改后的恢复
修改数据
1 | update test.test set id = 2 where id = 1 ; |
查看
反向修改错了反向后的语句如下
导入即可恢复
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
2018-01-17 Python全栈day20(装饰器基本理论)