引言 最近项目即将上线,收到任务先行在正式环境上备份还原库;于是有了以下命令:
mysqldump -u root -p xxxx > xxxx20241012.sql
使用标准mysqldump
备份 sql文件;然后再执行:
以下为错误示范
mysql -u root -p xxx < xxxx20241012.sql
以上命令请认真仔细看好,每个字都需确认清楚;
毫无疑问,这句命令犯了一个巨大错误(备份还原不能对一个表操作 )
正确命令为:
mysql -u root -p xxx2 < xxxx20241012.sql
由于错误的命令,我使用dump备份覆盖了线上数据库;而且由于还原耗时非常长;导致数据库丢失了2H的数据
本文介绍我的binlog数据还原过程;
解决方案 首先声明我的mysql版本是8025 ,服务所用框架为 spring boot
数据存储使用mybatis
很多教程都写的是用position 来还原数据,这当然是标准操作,毕竟有mysqlbinlog
命令原生支持
假如操作简单这么做当然省事效果好;但我其实是还原覆盖了数据库两次,服务器上生成了多个binlog文件;而且正式环境数据太多,我无法找到正确起止pos ,我更担心这个操作会再次对数据库的数据产生不利影响;进而产生更糟糕的结果;
因此我决定退而求其次,放弃全表还原,仅从binlog中 找到重要数据 的SQL语句,仅还原重要数据.
虽然,数据库被之前的备份还原导致中间的数据丢失;但在还原的过程中;服务正常运行,数据还是曾经写入到了数据库中;
因此binlog会有正常流程产生的数据 ;因此只需找到此时的INSERT 和UPDATE语句即可修复数据问题.
数据处理过程中涉及非常多的步骤,由于我非专业dba,linux命令也不熟;多亏的chatgpt,很多问题 它能提供能运行的代码;为我节省了大量时间;强烈建议多用用.
操作步骤 找到异常时数据库所有SQL记录
首先,找到mysql 服务器数据存放路径;查看异常binlog文件,由于我多次还原库因此我这里会有多个binlog
ls /xxx/mysql -all
xxxx 14:00 binlog.000048
xxxx 15:00 binlog.000049
xxxx 16:00 binlog.000050
xxxx 17:00 binlog.000051
xxxx 18:00 binlog.000052
还原数据之前,可以先将binlog文件 下载到本地,然后用本地的mysqlbinlog
命令解密及搜索处理;因为我这里数据量巨大而且完整解密出SQL语句之后的文件同样非常大;如此大的文件,操作建议本地在SSD上进行.
可以直接用less 查看 binlog;但是里面的SQL语句 base64加密了,因此使用以下命令解密
1 >mysqlbinlog --stop-datetime="2024-10-14 16:00:00" binlog.000050 --base64-output=decode-rows --verbose >coded_after50.sql
因为我这里数据多为方便追踪,每个binlog按照序号分别解密
1 2 ...coded_after51.sql ...coded_after52.sql
使用less
查看数据内容:
less code_after50.sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #241012 16 :29 :35 server id 1 end_log_pos 485557996 CRC32 0x1bc4ffbf Table_map: `xxx`.`yyyy` mapped to number 212 # at 485557996 #241012 16 :29 :35 server id 1 end_log_pos 485558475 CRC32 0x7448527f Update_rows: table id 212 flags: STMT_END_F ### UPDATE `xxx`.`yyyy` ### WHERE ### @1 = zzz ### @2 = '2024-10-12 16:29:25' ### @3 = 'xxxx' ### @4 = NULL ### @5 = NULL ### @6 = 0 ### @7 = NULL ### @8 = '33' ### @9 = NULL
以上为我解密的sql文件示例;每个binlog文件都有大量的SQL语句,每个文件都大于2G;在windows中 即使使用logview
也极难找到我想要的数据;
但linux 的grep
却极其好用高效;因此我在powershell
中输入bash
切换到wsl
环境中来操作.
筛选出目标表的操作SQL语句
假如你直接执行grep 'yyyy' coded_after50.sql
会发现大量不需要的数据,且没有上下文无法确定;此时你向ChatGPT求助;它会告诉你正确的命令;一番修改后就得到这样的语句
grep -A 50 yyyy decoded_after50.sql | grep -A 50 -B 25 "@2='2024-10-12"
此处解释一下:
-A 50
表面返回表后面50行数据,方便查看是否是需要的SQL语句
| grep
两次搜索 是从相关的表中找到出现问题时执行的SQL语句
-B 25
返回二次搜索关键字前25行,方便后续处理
@2='2024-10-12'
是我表中的创建时间,添加此条件后只返回当天的数据,如果想要过滤详细时间
可以使用-E "@2='2024-10-12 15:[4-5]"
使用正则查找15点40之后的数据
使用-E "@2='2024-10-12 1[6-8]"
查找16-18点的数据
多次调整过滤条件后添加 > 50.sql
将结果写入到文件中 方便后续处理
所以现在的命令是这样的
grep -A 50 yyyy decoded_after50.sql | grep -A 50 -B 25 -E "@2='2024-10-12 15:[4-5]" >50.SQL
假如有多个文件想要合并到一起.可以使用>>
操作符,如:
grep -A 50 yyyy decoded_after51.sql | grep -A 50 -B 25 -E "@2='2024-10-12 15:[4-5]" >>50.SQL
表示附加binlog51中的查找结果到50.SQL中
根据你的业务场景,你可以从50.SQL
中找到大致的SQL语句,但是由于服务使用了mybatis
导致有很多@符号,且文件中包括很多非SQL字符串,要转成所需SQL还需进一步处理
我这里给出我的简单处理的代码,使用C# 创建控制台即可使用
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 using Dapper;using MySqlConnector;using System.Data;using System.Data.SqlClient;using static System.Net.Mime.MediaTypeNames;using System.Text.RegularExpressions;namespace ConsoleApp1 { internal class Program { static void Main (string [] args ) { string connectionString = "server=xxxx" ; using (IDbConnection connection = new MySqlConnection(connectionString)) { var users = connection.Query<User>("show columns from xxx;" ).Select(s => s.Field).ToList(); string filePath; string directoryPath = @"G:\data\sql\" ; long maxSizeInBytes = 500 * 1024 ; string [] files = Directory.GetFiles(directoryPath); var smallFiles = files.Where(file => { var info = new FileInfo(file); return info.Length < maxSizeInBytes && info.Extension == ".sql" ; }); foreach (var item in smallFiles) { filePath = item; var fileINfo = new FileInfo(filePath); var isInsert = false ; string outputFilePath = fileINfo.Name; using (StreamReader reader = new StreamReader(filePath)) { using (StreamWriter writer = new StreamWriter(outputFilePath)) { string line; var listColumnName = new List<string >(); while ((line = reader.ReadLine()) != null ) { int index = line.IndexOf("###" ); var tableIndex = false ; var needColumns = false ; if (index >= 0 ) { Match match = Regex.Match(line, @"`(\w+)`.`(\w+)`" ); if (match.Success) { listColumnName = connection.Query<User>($"show columns from {match.Groups[2 ].Value} ;" ).Select(s => s.Field).ToList(); if (line.Contains("INSERT INTO" )) { needColumns = true ; isInsert=true ; } else { isInsert = false ; } tableIndex = true ; } string replacement = line.Substring(index + 3 ).Trim(); string result; if (isInsert) { string pattern = @"@\d+=" ; result = Regex.Replace(replacement, pattern, "" ); } else { string pattern = @"@(\d+)" ; MatchEvaluator evaluator = match => { int index2 = int .Parse(match.Groups[1 ].Value) - 1 ; if (index2 >= 0 && index2 < listColumnName.Count) { return listColumnName[index2]; } else { return match.Value; } }; result = Regex.Replace(replacement, pattern, evaluator); } line = line.Replace(line.Substring(index), result + (needColumns ? $"({string .Join("," , listColumnName)} )" : "," )); } else if (tableIndex) { tableIndex = false ; listColumnName = new List<string >(); isInsert= false ; } writer.WriteLine(line); } } } } } } public class User { public string Field { get ; set ; } } } }
处理后在bin目录即可看到同名文件只需少量复制粘贴即可有完整SQL
这里,假如你想要标识哪些SQL没写入;可以使用grepwin
搜索表名;然后根据行号逐个复制,防止遗漏
当然,你也可以写你自己的文本处理方法,只要调用show columns from xxx;
找到表对应的字段 即可替换出正确的SQL
数据还原 按照以上的步骤找出所有表相关的SQL语句之后,我从正式库同步一份目标表结构的空库到本地;
然后依时间执行SQL语句,即可得到最终结果的表;
中间因为大文本搜索,可能会有重复数据,本地执行SQL后数据库会帮我们检查数据的正确性,
根据逻辑,检查数据缺失或者关联的表数据
全部流程完成之后;
在本地的新测试库即可得到完整的修复数据拷贝,此时将所有数据导出成SQL语句
再补充上事务代码:
1 2 3 START TRANSACTION; ...INSERT COMMIT ;
多次检查之后,就能上线修复了.
参考资料