mysql 错误操作整个表全部数据后如何恢复?(百万行SQL,通过binlog日志恢复)
事件起因
事情起因:以为某个表里的数据都是系统配置的数据,没有用户数据,一个字段需要覆盖替换为新的url链接,直接写下了update t_xxx set xxx = 'https://xxxxxxxxx' ,然后执行了,执行的时候IDEA还提示这是危险操作,我思考了下不危险,这个就是要全部覆盖,然后就点击了execute确认执行,于是翻车了,等我覆盖完去看效果的时候,惊奇的发现用户数据也被覆盖了,于是去看这个表里的数据,真有用户数据,但是值得高兴的是,全被覆盖了。。。。真是手贱啊,小心了一万年还是翻车了,全覆盖了跟删了没多大区别~~~~都是闯祸
既然事情已经发生,那就开始修复把~
找Binlog文件位置
binlog文件是mysql的更新记录,二进制形式。可以用于恢复数据。
这个文件每1个G为一个文件,满 了就新建一个,所以mysql运行久了这个binlog文件很多个,每个1G,建议跟我一样设置滚动删除,我是最多保留最近8个。(实验机器配置低~)
我的是mysql 8,ubuntu,我的binlog文件在这个目录
shell
/var/lib/mysql
聪明的你一眼就能看出这个binlog 27号文件只有六百多兆,序号也是最新的,证明这个是最近的一个文件,就是他了,冲
读取指定时间范围内的数据操作DML
我直接查看了我这冤种操作的时间,发现大概是7分的时候操作的覆盖调数据,于是我把那一分钟里的SQL导出来(自行评估时间范围),导出为一个SQL文件
备注:mysqlbinlog工具是mysql自带的
shell
mysqlbinlog --base64-output=DECODE-ROWS --verbose --start-datetime="2023-12-23 01:00:00" --stop-datetime="2023-12-23 01:10:00" /var/lib/mysql/binlog.000027 > restore.sql
把导出的SQL清理数据
导出的sql,我用IDEA打开的,随便你用哪个好用的文本处理工具,我们直接搜索set这个词,就能找到SQL的最开始位置如图
例如一条
sql
### UPDATE `xx`.`t_xx`
### WHERE
### @1=1
### @2='xx'
### @3=NULL
### @4='默认'
### @5='xx'
### @6='xxxx'
### @7='1'
### @8=1
### @9='2023-08-19 15:29:58'
### @10='2023-08-19 18:41:06'
### SET
### @1=1
### @2='xx'
### @3='xx'
### @4='默认'
### @5='xx'
### @6='xx'
### @7='1'
### @8=1
### @9='2023-08-19 15:29:58'
### @10='2023-12-23 01:07:09'
这个数据的格式不难看出,记录了变更之前和之后的每个字段数据,xx为示意作用,在真实场景中就是你的真实数据,字段根据顺序@1一直到@10,证明我有10个字段,然后没个字段的值都记录了,整个结构为
update 表
where
每个字段的旧值
set
每个字段的新值
那么我们现在开始处理数据
IDEA 对于这个26MB的文件直接变仅读模式,不给我编辑,于是打开了subline text这个工具处理文本
处理数据
删除update语句以外的所有无关行
找到这句错SQL的所有update语句,其它无关行全部删除
我们要的核心就是这个update语句,我们先掐头去尾,保留中间的update,我这个文件目前有100万行~
现在我这个文件只有那一个失误SQL执行的所有update语句,总共92万行~
去掉开头的###
直接搜索把###空格,这四个字符,直接替换为空字符,于是电脑卡了一会,,,电脑这会汗流浃背,风扇狂扇,过了会好像没响应,算了撸代码解决
java
/**
* author: humorchen
* date: 2023/12/23/023 1:57
* desc:
**/
public class RestoreData {
/**
* 移除头部标识
*
* @throws Exception
*/
public static void removeHeadSignal() throws Exception {
String file = "C:\\Users\\Administrator\\Desktop\\restore.sql";
String newFile = "C:\\Users\\Administrator\\Desktop\\restore_1.sql";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(newFile));
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
if (line.startsWith("### ")) {
line = line.substring(4);
}
bufferedWriter.write(line);
bufferedWriter.write("\n");
}
bufferedWriter.flush();
bufferedWriter.close();
bufferedReader.close();
}
public static void main(String[] args) throws Exception {
removeHeadSignal();
}
}
set和where互换
这个互换简直不要太简单,轻松的很
java
/**
* where和set互换
*
* @throws Exception
*/
public static void reverseWhereAndSet() throws Exception {
String file = "C:\\Users\\Administrator\\Desktop\\restore_1.sql";
String newFile = "C:\\Users\\Administrator\\Desktop\\restore_2.sql";
final String SET = "SET";
final String WHERE = "WHERE";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(newFile));
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
if (line.startsWith(WHERE)) {
line = SET;
} else if (line.startsWith(SET)) {
line = WHERE;
}
bufferedWriter.write(line);
bufferedWriter.write("\n");
}
bufferedWriter.flush();
bufferedWriter.close();
bufferedReader.close();
}
public static void main(String[] args) throws Exception {
// removeHeadSignal();
reverseWhereAndSet();
}
字段替换
把字段数组作为参数数组传入函数,然后读取@多少,替换为对应下表的字段名
java
/**
* 替换字段名
*
* @param columnNames
*/
public static void replaceColumns(String[] columnNames) throws Exception {
String file = "C:\\Users\\Administrator\\Desktop\\restore_2.sql";
String newFile = "C:\\Users\\Administrator\\Desktop\\restore_3.sql";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(newFile));
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
String line = null;
final String PREFIX = " @";
final int PREFIX_LEN = PREFIX.length();
while ((line = bufferedReader.readLine()) != null) {
// line: @1=1
if (line.length() > PREFIX_LEN && line.startsWith(PREFIX)) {
// 等于号位置
int eqIndex = line.indexOf("=");
String indexStr = line.substring(PREFIX_LEN, eqIndex);
if (StrUtil.isNumeric(indexStr)) {
int index = Integer.parseInt(indexStr) - 1;
if (index >= 0 && index < columnNames.length) {
// 替换字段名
line = line.replace(PREFIX + indexStr, columnNames[index]);
}
}
}
bufferedWriter.write(line);
bufferedWriter.write("\n");
}
bufferedWriter.flush();
bufferedWriter.close();
bufferedReader.close();
}
public static void main(String[] args) throws Exception {
// removeHeadSignal();
// reverseWhereAndSet();
replaceColumns(new String[]{"id", "title", "icon", "type", "email", "prompt", "temperature", "keep_context", "create_time", "update_time"});
}
加,和and
看到最后你就发现只缺标点符号,;了,那就加上,起初想去判断代码处于update、set、where三个区域里加不同的,又想到要判定最后一个不能加标点,要加空格,最后一个又是分号; 真麻烦!直接搞个后缀数组算了,反正我这个表结构是固定的于是用以下代码加上标点符号和换行。
java
/**
* 添加,和and和;
*
* @throws Exception
*/
public static void addPrefix(String[] addPrefix) throws Exception {
String file = "C:\\Users\\Administrator\\Desktop\\restore_3.sql";
String newFile = "C:\\Users\\Administrator\\Desktop\\restore_4.sql";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(newFile));
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
String line = null;
int index = -1;
final String UPDATE = "UPDATE";
while ((line = bufferedReader.readLine()) != null) {
if (line.startsWith(UPDATE)) {
index = 0;
bufferedWriter.write("\n\n");
;
}
if (index >= 0) {
line = line + addPrefix[index];
index++;
}
bufferedWriter.write(line);
bufferedWriter.write("\n");
}
bufferedWriter.flush();
bufferedWriter.close();
bufferedReader.close();
}
public static void main(String[] args) throws Exception {
// removeHeadSignal();
// reverseWhereAndSet();
// replaceColumns(new String[]{"id", "title", "icon", "type", "email", "prompt", "temperature", "keep_context", "create_time", "update_time"});
addPrefix(new String[]{" ", " ", ",", ",", ",", ",", ",", ",", ",", ",", ",", " ", " ", " and ", " and ", " and ", " and ", " and ", " and ", " and ", " and ", " and ", " ;"});
}
大功告成,拿去执行即可!
执行之前你先随机取个三条执行一下看看,确认自己的SQL生产对了。
我把sql上传到了ubuntu,进入mysql -uroot -p,然后use xxx;source xxx.sql;执行完数据恢复了,会有点久,如果数据量大,执行会更久。珍惜数据。。。
我这个一百万行的恢复数据SQL大约执行了3、5分钟执行完了。数据全部恢复到错误SQL前的数据了。