MySQL误删数据怎么办?

一、背景

某天,张三打算操作数据库,删除自己项目的无用数据,但是一不小心数据删多了。被误删的数据,如何恢复呢?本文将介绍相关方法,以及现成的一些工具。

例子:

有一个表

sql 复制代码
create table person
(
  id   bigint primary key auto_increment comment 'id',
  name varchar(50) comment '名称'
) engine = innodb;

原本是要执行这条SQL语句:

sql 复制代码
delete from person where id > 500000;

不小心执行了这条SQL语句:

sql 复制代码
delete from person;

二、解决方案

处理这个问题的解决思路就是,基于binlog找回被删除的数据,将被删除的数据重新插入到数据库。

对于binlog文件来说,实际上保存的是对于数据库的正向操作。比如说,插入数据insert,binlog中保存的也是insert语句;删除数据delete,binlog中保存的也是delete语句。

因此,想要恢复被删除的数据,主要有两种方式:

描述 优点 缺点
找到数据插入的位置,重新执行数据的插入操作 1. 比较方便,不需要生成逆向操作,直接执行sql脚本重新插入数据即可 2. 对binlog的模式没有限制,row模式、statement模式都能找到具体的数据 1. 如果数据插入之后还有更新操作,插入的数据不是最新的,会有问题 2. 如果被删除的数据比较多,插入的位置比较多,找到插入的位置比较困难
找到数据被删除的位置,生成逆向操作,重新执行插入操作 1. 只要找到数据被删除的位置即可找到所有被删除的数据,比较方便 1. 需要通过脚本生成逆向操作,才能将数据恢复 2. 需要保证binlog模式是row模式,才能找到被删除的数据。否则,statement模式不会找到具体的数据

下面就针对上面的两种方式,进行详细的讲解

1. 通用操作

首先介绍两种方式都需要使用到的一些通用的操作,主要用于设置binlog、找到binlog文件

1.1 确认binlog开启

1.1.1 查询开启状态

首先要保证binlog是开启的,不然数据肯定是没办法恢复回来的。

在MySQL中,可以通过执行以下SQL查询来检查是否已经开启了binlog:

sql 复制代码
SHOW VARIABLES LIKE 'log_bin';

这个查询将返回一个结果集,其中包含名为log_bin的系统变量的值。如果log_bin的值为ON,则表示binlog已经开启;如果值为OFF,则表示binlog没有开启。

shell 复制代码
mysql> SHOW VARIABLES LIKE 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+
1 row in set (0.01 sec)

1.1.2 开启binlog

如果发现没有开启,可以通过修改MySQL配置文件(通常是my.cnfmy.ini,Linux下MySQL的配置文件目录一般是/etc/mysql)中的[mysqld]部分来开启binlog。如果在配置文件中找到了类似以下的设置,则表示binlog已经开启:

properties 复制代码
[mysqld]
log-bin=mysql-bin
server-id=1
  1. 修改配置启用了binlog之后,需要重启MySQL服务才能使更改生效
  2. mysql-bin表示binlog文件的前缀
  3. server-id 设置了MySQL服务器的唯一ID,必须设置ID,否则没办法开启binlog

1.2 binlog模式

刚刚提到,对于delete操作,只有row模式才能找到被删除数据的具体值,因此需要确认开启的binlog模式。

1.2.1 查询binlog模式

要查询MySQL的binlog模式,您可以使用以下SQL命令:

sql 复制代码
SHOW VARIABLES LIKE 'binlog_format';

这将返回一个结果集,其中包含当前的binlog格式。可能的值有:

  • ROW:表示使用行模式(row-based replication),这是推荐的设置,因为它提供了更好的数据一致性。
  • STATEMENT:表示使用语句模式(statement-based replication),在这种模式下,可能会丢失一些数据,因为它仅记录执行的SQL语句。
  • MIXED:表示混合模式(mixed-based replication),在这种模式下,MySQL会根据需要自动切换行模式和语句模式。

1.2.2 配置binlog模式

可以通过修改MySQL配置文件(通常是my.cnfmy.ini,Linux下MySQL的配置文件目录一般是/etc/mysql)中的[mysqld]部分来修改binlog模式。

[mysqld]部分下,添加或修改以下行,将binlog_format设置为想要的模式(ROWSTATEMENTMIXED):

properties 复制代码
[mysqld]
binlog_format=ROW

随后重启mysql服务使其生效

1.3 binlog信息查询

通过以下操作,我们可以找到binlog文件

1.3.1 查询当前使用的binlog文件

通过show master status;可以找到当前正在使用的binlog文件

shell 复制代码
mysql> show master status\G
*************************** 1. row ***************************
             File: mysql-bin.000217
         Position: 668127868
     Binlog_Do_DB: 
 Binlog_Ignore_DB: 
Executed_Gtid_Set: 29dc2bf9-f657-11ee-b369-08c0eb829a3c:1-291852745,
744ca9cd-5f86-11ef-98d6-0c42a131d16f:1-5374311
1 row in set (0.00 sec)

1.3.2 找到所有binlog文件名

show master logs;可以找到所有binlog文件名

shell 复制代码
mysql> show master logs;
+------------------+------------+
| Log_name         | File_size  |
+------------------+------------+
| mysql-bin.000200 | 1073818388 |
| mysql-bin.000201 | 1073757563 |
| mysql-bin.000202 | 1074635635 |
| mysql-bin.000203 | 1073801053 |
| mysql-bin.000204 | 1073856643 |
| mysql-bin.000205 | 1073910661 |
| mysql-bin.000206 | 1073742603 |
| mysql-bin.000207 | 1195256434 |
| mysql-bin.000208 | 1085915611 |
| mysql-bin.000209 | 1073990985 |
| mysql-bin.000210 | 1075942323 |
| mysql-bin.000211 | 1074716392 |
| mysql-bin.000212 | 1073763938 |
| mysql-bin.000213 | 1073780482 |
| mysql-bin.000214 | 1074029712 |
| mysql-bin.000215 | 1073832842 |
| mysql-bin.000216 | 1079999184 |
| mysql-bin.000217 |  668173793 |
+------------------+------------+

1.3.3 查询binlog保存位置

SHOW VARIABLES LIKE 'log_bin_basename'; 可以找到binlog文件保存的目录位置。比如说/var/lib/mysql/mysql-bin表示目录为/var/lib/mysql/下的以mysql-bin为前缀的文件。

我们通过文件的最后修改时间,可以看出binlog覆盖的时间范围。一般后缀的数字越大,表示越新。

shell 复制代码
mysql> SHOW VARIABLES LIKE 'log_bin_basename';
+------------------+--------------------------+
| Variable_name    | Value                    |
+------------------+--------------------------+
| log_bin_basename | /var/lib/mysql/mysql-bin |
+------------------+--------------------------+
1 row in set (0.00 sec)
shell 复制代码
bash-4.2# ls /var/lib/mysql/mysql-bin* -alh
-rw-r----- 1 mysql mysql 1.1G Sep  9 02:28 /var/lib/mysql/mysql-bin.000200
-rw-r----- 1 mysql mysql 1.1G Sep  9 02:32 /var/lib/mysql/mysql-bin.000201
-rw-r----- 1 mysql mysql 1.1G Sep  9 02:39 /var/lib/mysql/mysql-bin.000202
-rw-r----- 1 mysql mysql 1.1G Sep  9 02:45 /var/lib/mysql/mysql-bin.000203
-rw-r----- 1 mysql mysql 1.1G Sep  9 07:52 /var/lib/mysql/mysql-bin.000204
-rw-r----- 1 mysql mysql 1.1G Sep  9 12:10 /var/lib/mysql/mysql-bin.000205
-rw-r----- 1 mysql mysql 1.1G Sep 10 04:40 /var/lib/mysql/mysql-bin.000206
-rw-r----- 1 mysql mysql 1.2G Sep 10 07:00 /var/lib/mysql/mysql-bin.000207
-rw-r----- 1 mysql mysql 1.1G Sep 11 07:54 /var/lib/mysql/mysql-bin.000208
-rw-r----- 1 mysql mysql 1.1G Sep 12 03:03 /var/lib/mysql/mysql-bin.000209
-rw-r--r-- 1 root  root   24M Sep 11 09:06 /var/lib/mysql/mysql-bin.000209.event.log
-rw-r----- 1 mysql mysql 1.1G Sep 12 03:30 /var/lib/mysql/mysql-bin.000210
-rw-r----- 1 mysql mysql 1.1G Sep 12 08:33 /var/lib/mysql/mysql-bin.000211
-rw-r----- 1 mysql mysql 1.1G Sep 12 08:35 /var/lib/mysql/mysql-bin.000212
-rw-r----- 1 mysql mysql 1.1G Sep 12 22:00 /var/lib/mysql/mysql-bin.000213
-rw-r----- 1 mysql mysql 1.1G Sep 13 10:26 /var/lib/mysql/mysql-bin.000214
-rw-r----- 1 mysql mysql 1.1G Sep 13 10:29 /var/lib/mysql/mysql-bin.000215
-rw-r----- 1 mysql mysql 1.1G Sep 14 01:42 /var/lib/mysql/mysql-bin.000216
-rw-r----- 1 mysql mysql 637M Sep 14 06:11 /var/lib/mysql/mysql-bin.000217
-rw-r----- 1 mysql mysql 4.1K Sep 14 01:42 /var/lib/mysql/mysql-bin.index

2. 方案一:找到insert语句,重新插入

需要执行以下几个步骤:

  1. 确认insert插入数据的时间,找到对应的binlog文件
  2. 解析该binlog文件,指定时间点,在binlog文件中找到插入数据的位置
  3. 重新解析binlog文件,指定binlog位置。对解析出来的文件进行重放。

2.1 找到binlog文件

比如说,数据是在9月12日12:00插入的,那么我们看上方的所有binlog文件,可以看出插入语句应该保存在mysql-bin.000213文件中。

2.2 根据时间点解析binlog文件

通过mysqlbinlog将binlog文件解析成可读的sql文件。

shell 复制代码
mysqlbinlog --base64-output=decode-rows -v --start-datetime="2024-09-12 11:59:00" --stop-datetime="2024-09-12 12:01:00" mysql-bin.000213 > binlog.sql
  • --base64-output=decode-rows:将二进制日志中的行事件解码为 SQL 语句。
  • -v--verbose:输出详细的事件信息。
  • --start-datetime="2024-09-12 11:59:00":从指定的日期和时间开始读取二进制日志。通过指定时间范围,可以减小解析出来的sql文件,避免太多无用信息使得查询位置比较困难。
  • --stop-datetime="2024-09-12 12:01:00":在指定的日期和时间停止读取二进制日志。
  • mysql-bin.000213:要解析的二进制日志文件的路径和名称。
  • >:将命令的输出重定向到指定的文件。
  • binlog.sql:保存解码后的 SQL 语句的文件名。

2.2.1 statement模式确认binlog位置

我们可以找到insert into person values (1, 'first'),并且分别在前后的BEGINCOMMIT找到position。

BEGIN往前找有一个position at 219COMMIT往后找有一个position at 445,这就是插入语句的实际binlog范围。

sql 复制代码
# at 219
#240914 17:14:26 server id 1  end_log_pos 300 CRC32 0xb8159bc1 	Query	thread_id=1267	exec_time=0	error_code=0
SET TIMESTAMP=1726305266/*!*/;
SET @@session.pseudo_thread_id=1267/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1436549120/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C latin1 *//*!*/;
SET @@session.character_set_client=8,@@session.collation_connection=8,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
BEGIN
/*!*/;
# at 300
#240914 17:14:26 server id 1  end_log_pos 414 CRC32 0xb7e0263b 	Query	thread_id=1267	exec_time=0	error_code=0
use `tests`/*!*/;
SET TIMESTAMP=1726305266/*!*/;
insert into person values (1, 'first')
/*!*/;
# at 414
#240914 17:14:26 server id 1  end_log_pos 445 CRC32 0x9345e6ca 	Xid = 30535
COMMIT/*!*/;
# at 445

2.2.2 row模式确认binlog位置

row模式下,与statement模式下的binlog格式有少许差别,但方法是一致的。

我们可以找到以 ###开头的几行,包含INSERT INTO语句。并且分别在前后的BEGINCOMMIT找到position。

BEGIN往前找有一个position at 219COMMIT往后找有一个position at 426,这就是插入语句的实际binlog范围。

sql 复制代码
# at 219
#240914 17:16:36 server id 1  end_log_pos 292 CRC32 0xe9082d52 	Query	thread_id=20	exec_time=0	error_code=0
SET TIMESTAMP=1726305396/*!*/;
SET @@session.pseudo_thread_id=20/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1436549120/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C latin1 *//*!*/;
SET @@session.character_set_client=8,@@session.collation_connection=8,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
BEGIN
/*!*/;
# at 292
#240914 17:16:36 server id 1  end_log_pos 345 CRC32 0x1832ced4 	Table_map: `tests`.`person` mapped to number 111
# at 345
#240914 17:16:36 server id 1  end_log_pos 395 CRC32 0x32d6a21b 	Write_rows: table id 111 flags: STMT_END_F
### INSERT INTO `tests`.`person`
### SET
###   @1=1
###   @2='first'
# at 395
#240914 17:16:36 server id 1  end_log_pos 426 CRC32 0x07619928 	Xid = 149
COMMIT/*!*/;
# at 426

2.3 根据binlog位置解析binlog文件

上一步,我们已经找到了具体的位置,现在我们可以重新解析binlog文件,指定binlog位置。内容和上方实际上没有太大差异。

shell 复制代码
mysqlbinlog --start-position=219 --stop-position=426 mysql-bin.000213 > binlog.sql

需要注意的是,这个解析语句,删掉了--base64-output=decode-rows -v 参数。因为这些参数是用于解码binlog的,是让开发人员更方便看到binlog被解析之后的格式。但是对mysql来说是没办法使用的。

2.4 重放数据

解析的这个文件就是一个sql脚本文件,通过往常的方式执行sql脚本即可

shell 复制代码
mysql -uroot -proot < binlog.sql

或者mysql客户端登陆之后,通过source命令执行

sql 复制代码
source binlog.sql;

3. 方案二:找到delete语句,生成逆向操作,重新insert

3.1 找到binlog文件

比如说,数据是在9月12日12:00删除的,那么我们看上方的所有binlog文件,可以看出插入语句应该保存在mysql-bin.000213文件中。

3.2 根据时间点解析binlog文件

操作和上方2.2的操作没有差异,我们主要来比较一下statement模式和row模式的差别。我们会发现statement模式下,没办法找到所有被删除的数据的具体数据,而row模式能找到。

3.2.1 statement模式

我们可以看到binlog只保存了一句 delete from person。很遗憾,啥数据都没有,也没办法根据它生成逆向操作。

sql 复制代码
# at 445
#240914 17:15:13 server id 1  end_log_pos 510 CRC32 0x6a7a66e4 	Anonymous_GTID	last_committed=1	sequence_number=2	rbr_only=no
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 510
#240914 17:15:13 server id 1  end_log_pos 591 CRC32 0x55e4225b 	Query	thread_id=1267	exec_time=0	error_code=0
SET TIMESTAMP=1726305313/*!*/;
BEGIN
/*!*/;
# at 591
#240914 17:15:13 server id 1  end_log_pos 685 CRC32 0x10938b9d 	Query	thread_id=1267	exec_time=0	error_code=0
SET TIMESTAMP=1726305313/*!*/;
delete from person
/*!*/;
# at 685
#240914 17:15:13 server id 1  end_log_pos 716 CRC32 0x1ea4a681 	Xid = 30610
COMMIT/*!*/;
# at 716

3.2.2 row模式

可以看到binlog以 ###开头的几行,WHERE之后,把被删除数据的每一个字段都作为条件嵌入到sql语句中了。条件的顺序,就是表结构的字段顺序。比如说@1对应的就是id@2对应的就是name

sql 复制代码
# at 1574
#240914 17:16:38 server id 1  end_log_pos 1642 CRC32 0x944b1b94 	Query	thread_id=20	exec_time=1260	error_code=0
SET TIMESTAMP=1726305398/*!*/;
BEGIN
/*!*/;
# at 1642
#240914 17:16:38 server id 1  end_log_pos 1695 CRC32 0x435282e2 	Table_map: `tests`.`person` mapped to number 111
# at 1695
#240914 17:16:38 server id 1  end_log_pos 1745 CRC32 0x3063bf8c 	Delete_rows: table id 111 flags: STMT_END_F
### DELETE FROM `tests`.`person`
### WHERE
###   @1=1
###   @2='first'
# at 1745
#240914 17:16:38 server id 1  end_log_pos 1776 CRC32 0x086c2270 	Xid = 3391
COMMIT/*!*/;

3.3 生成逆向操作

根据上面的binlog文件,我们可以通过脚本生成逆向操作。

sql 复制代码
insert into person values (1, 'first');

3.4 重放数据

与 2.4 一致

三、常见工具

目前有一些开源的工具,可以帮助我们解析binlog,并且自动生成binlog记录的操作的逆向操作。

1. binlog2mysql

binlog2sql由美团点评DBA团队(上海)出品,python脚本实现。主要原理是伪装成slave,向master获取binlog,并且根据binlog生成逆向操作。

下载地址:GitHub - danfengcao/binlog2sql: Parse MySQL binlog to SQL you want

在执行之前,需要确认mysql server已设置以下参数:

properties 复制代码
[mysqld]
server_id = 1
log_bin = /var/log/mysql/mysql-bin.log
max_binlog_size = 1G
binlog_format = row
binlog_row_image = full

获取正向操作:

shell 复制代码
> python binlog2sql.py -h127.0.0.1 -P13306 -uroot -p --start-file=mysql-bin.000002
Password:
INSERT INTO `tests`.`person`(`id`, `name`) VALUES (1, 'first'); #start 4 end 395 time 2024-09-14 17:16:36
DELETE FROM `tests`.`person` WHERE `id`=1 AND `name`='first' LIMIT 1; #start 426 end 667 time 2024-09-14 17:16:38
  1. 通过命令,输入用户名、密码、端口号、地址等,并且指定binlog文件
  2. 通过输出,可以看出所有正向操作,以及每个正向操作的时间、binlog位置

获取逆向操作:

shell 复制代码
> python binlog2sql.py -h127.0.0.1 -P13306 -uroot -p --start-file=mysql-bin.000002 --flashback
Password:
INSERT INTO `tests`.`person`(`id`, `name`) VALUES (1, 'first'); #start 426 end 667 time 2024-09-14 17:16:38
DELETE FROM `tests`.`person` WHERE `id`=1 AND `name`='first' LIMIT 1; #start 4 end 395 time 2024-09-14 17:16:36
  1. 命令中,新增一个参数 --flashback,用于指定回滚
  2. 通过输出,可以看出所有逆向操作。并且可以看出相对于正向操作来说,逆向操作的顺序是相反的,按时间从后往前排序

还有其他工具,比如说MyFlash等,大家可以自行研究

MyFlash:GitHub - Meituan-Dianping/MyFlash: flashback mysql data to any point

四、总结

  1. 我们可以通过binlog找回误删的数据,前提是开启了binlog。建议binlog模式为row模式,否则没办法根据正向操作生成逆向操作。
  2. 有一些开源工具可以自动解析binlog,并且生成逆向操作。
相关推荐
掘金码甲哥2 分钟前
调试grpc的哼哈二将,你值得拥有
后端
小学鸡!16 分钟前
Spring Boot实现日志链路追踪
java·spring boot·后端
yumgpkpm41 分钟前
华为鲲鹏 Aarch64 环境下多 Oracle 、mysql数据库汇聚到Cloudera CDP7.3操作指南
大数据·数据库·mysql·华为·oracle·kafka·cloudera
程序员云帆哥2 小时前
MySQL JDBC Driver URL参数配置规范
数据库·mysql·jdbc
用户21411832636022 小时前
OpenSpec 实战:用规范驱动开发破解 AI 编程协作难题
后端
Olrookie2 小时前
若依前后端分离版学习笔记(二十)——实现滑块验证码(vue3)
java·前端·笔记·后端·学习·vue·ruoyi
LucianaiB3 小时前
招聘可以AI面试,那么我制作了一个AI面试教练不过分吧
后端
无奈何杨3 小时前
CoolGuard更新,ip2region升级、名单增加过期时间
后端
三三木木七3 小时前
mysql拒绝连接
数据库·mysql
唐古乌梁海3 小时前
【mysql】InnoDB的聚簇索引和非聚簇索引工作原理
数据库·mysql