目录

一、前言

二、基于mysqlbinlog工具的误更新恢复

三、基于binlog2sql的误更新恢复


一、前言

        上周五,应用侧反应一个表在update时由于使用了左外连接。被误更新,导致全表数据丢失。反馈问题时间是上午11:30,误更新时间是10:20左右,此MYSQL数据库业务量较大,一小时的binlog日志量在2G左右。

        解决思路:首先需要对这个表进行锁表,避免再有其他的更新,增加恢复量。然后开始考虑使用mysqlbinlog对日志筛选、转换、回滚语句,但是将近4G的binlog需要处理,工作量巨大。后来听同事说有专门的binlog解析工具binlog2sql,基于Python编写的。于是网上搜索安装包,安装的过程中又遇到了一些权限和依赖的问题。好在最后成功安装了binlog2sql,生成了该表的全部回滚语句成功进行了恢复。但是由于之前没有Mysql误更新恢复的经验,第一次安装binlog2sql也遇到很多坑,导致核心业务表在误更新后下午18:00才恢复,造成业务长时间中断。于是梳理处理过程、模拟案例,希望下次遇到同样问题能缩短恢复时间。

二、基于mysqlbinlog工具的误更新恢复

1、测试环境

mysql> show variables like '%binlog_format%';   --binlog模式必须是ROW模式,否则无法恢复。
 +---------------+-------+
 | Variable_name | Value |
 +---------------+-------+
 | binlog_format | ROW   |
 +---------------+-------+
 1 row in set (0.00 sec)mysql> select version();
 +-----------+
 | version() |
 +-----------+
 | 8.0.26    |
 +-----------+
 1 row in set (0.00 sec)mysql>

2、测试数据

mysql> select count(*) from test;
 +----------+
 | count(*) |
 +----------+
 |    10000 |
 +----------+
 1 row in set (0.05 sec)mysql> desc test;
 +-----------+------+------+-----+---------+-------+
 | Field     | Type | Null | Key | Default | Extra |
 +-----------+------+------+-----+---------+-------+
 | emp_no    | int  | NO   |     | NULL    |       |
 | salary    | int  | NO   |     | NULL    |       |
 | from_date | date | NO   |     | NULL    |       |
 | to_date   | date | NO   |     | NULL    |       |
 +-----------+------+------+-----+---------+-------+
 4 rows in set (0.00 sec)mysql>

3、模拟误更新 

mysql> show master status;
 +-----------------+----------+--------------+------------------+--------------------------------------------+
 | File            | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                          |
 +-----------------+----------+--------------+------------------+--------------------------------------------+
 | mysqlbin.000001 |      156 |              |                  | d83239cd-ce27-11ec-b08e-080027e2598e:1-186 |
 +-----------------+----------+--------------+------------------+--------------------------------------------+
 1 row in set (0.00 sec)mysql> flush logs;   --先切换下binlog
 Query OK, 0 rows affected (0.01 sec)mysql> show master status;
 +-----------------+----------+--------------+------------------+--------------------------------------------+
 | File            | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                          |
 +-----------------+----------+--------------+------------------+--------------------------------------------+
 | mysqlbin.000002 |      156 |              |                  | d83239cd-ce27-11ec-b08e-080027e2598e:1-186 |
 +-----------------+----------+--------------+------------------+--------------------------------------------+
 1 row in set (0.00 sec)mysql> 
 mysql> update employees.test set salary=20000;  --这里不会写左连接的更新,就用忘写where条件的误更新模拟
 Query OK, 10000 rows affected (0.09 sec)
 Rows matched: 10000  Changed: 10000  Warnings: 0

4、锁表

mysql> lock table employees.test read;  --先锁表
 Query OK, 0 rows affected (0.00 sec)mysql>

5、筛选binlog中的update记录

[root@test2 log]#  mysqlbinlog --no-defaults -v -v --base64-output=DECODE-ROWS mysqlbin.000002 | grep  'UPDATE `employees`.`test`' | more | wc -l
20000
 [root@test2 log]#  mysqlbinlog --no-defaults -v -v --base64-output=DECODE-ROWS mysqlbin.000002 | grep -C 10  'UPDATE `employees`.`test`' | more
 # at 262646
 #220613  9:25:59 server id 11  end_log_pos 270842 CRC32 0x0f4fcdb0      Update_rows: table id 81
 # at 270842
 #220613  9:25:59 server id 11  end_log_pos 279038 CRC32 0x57ac6088      Update_rows: table id 81
 # at 279038
 #220613  9:25:59 server id 11  end_log_pos 287234 CRC32 0x4c698df0      Update_rows: table id 81
 # at 287234
 #220613  9:25:59 server id 11  end_log_pos 295430 CRC32 0xc1e70954      Update_rows: table id 81
 # at 295430
 #220613  9:25:59 server id 11  end_log_pos 301706 CRC32 0xea6a387f      Update_rows: table id 81 flags: STMT_END_F
 ### UPDATE `employees`.`test`
 ### WHERE
 ###   @1=10001 /* INT meta=0 nullable=0 is_null=0 */
 ###   @2=60117 /* INT meta=0 nullable=0 is_null=0 */
 ###   @3='1986:06:26' /* DATE meta=0 nullable=0 is_null=0 */
 ###   @4='1987:06:26' /* DATE meta=0 nullable=0 is_null=0 */
 ### SET
 ###   @1=10001 /* INT meta=0 nullable=0 is_null=0 */
 ###   @2=20000 /* INT meta=0 nullable=0 is_null=0 */
 ###   @3='1986:06:26' /* DATE meta=0 nullable=0 is_null=0 */
 ###   @4='1987:06:26' /* DATE meta=0 nullable=0 is_null=0 */
 ### UPDATE `employees`.`test`
 ### WHERE
 ###   @1=10001 /* INT meta=0 nullable=0 is_null=0 */
 ###   @2=62102 /* INT meta=0 nullable=0 is_null=0 */
 ###   @3='1987:06:26' /* DATE meta=0 nullable=0 is_null=0 */
 ###   @4='1988:06:25' /* DATE meta=0 nullable=0 is_null=0 */
 ### SET
 ###   @1=10001 /* INT meta=0 nullable=0 is_null=0 */
 ###   @2=20000 /* INT meta=0 nullable=0 is_null=0 */
 ###   @3='1987:06:26' /* DATE meta=0 nullable=0 is_null=0 */
 ###   @4='1988:06:25' /* DATE meta=0 nullable=0 is_null=0 */
 ### UPDATE `employees`.`test`
 ### WHERE
  。。。。

         如果误更新后立马发现了,立即对该表进行锁表,没有其他的更新,则可以使用基于事务的,及根据binlog里面的更新开始前的‘# at 295430’ 关键字和事务结束的COMMIT关键字截取筛选
 如果误更新后一段时间后发现了,这期间又有很多其他的更新,需要倒叙的方式根据‘# at xxx’ 依次回滚
         这两种情况对于没有切换binlog日志的误更新很合适,如果误更新后没有立即发现,然后对误更新的表进行了很多其他的更新,而且还切换了binlog日志,这种情况就都不合适了,可以使用下面的第二种方法,binlog2sql的分析工具,自动生成所有的回滚语句。
 

由于我们在误更新后换进行了其他的更新所以需要使用基于表的。

6、对筛选出来的误更新记录进行处理

sed '/WHERE/{:a;N;/SET/!ba;s/\([^\n]*\)\n\(.*\)\n\(.*\)/\3\n\2\n\1/}' update2.sql | sed 's/### //g;s/\/\*.*/,/g' | sed  /@4/s/,//g | sed '/WHERE/{:a;N;/@4/!ba;s/,/AND/g};s/#.*//g;s/COMMIT,//g' | sed '/^$/d' > rollback.sql
 --这一步是把update语句提取出来,去掉多余的标识符  这里面的@4要替换成该表的最后一列,一般表有几列就是@几
 [root@test2 log]# more rollback.sql 
 UPDATE `employees`.`test`
 SET
   @1=10001 ,
   @2=60117 ,
   @3='1986:06:26' ,
   @4='1987:06:26' 
 WHERE
   @1=10001 AND
   @2=20000 AND
   @3='1986:06:26' AND
   @4='1987:06:26' 
 UPDATE `employees`.`test`
 SET
   @1=10001 ,
   @2=62102 ,
   @3='1987:06:26' ,
   @4='1988:06:25' 
 WHERE
   @1=10001 AND
   @2=20000 AND
   @3='1987:06:26' AND
   @4='1988:06:25' 
 UPDATE `employees`.`test`[root@test2 log]#  sed  -i -r  '/WHERE/{:a;N;/@4/!ba;s/(@4=.*)/\1\;/g}' rollback.sql  --这一步是在where后面加;,切割update语句。
 [root@test2 log]# more rollback.sql 
 UPDATE `employees`.`test`
 SET
   @1=10001 ,
   @2=60117 ,
   @3='1986:06:26' ,
   @4='1987:06:26' 
 WHERE
   @1=10001 AND
   @2=20000 AND
   @3='1986:06:26' AND
   @4='1987:06:26' ;
 UPDATE `employees`.`test`
 SET
   @1=10001 ,
   @2=62102 ,
   @3='1987:06:26' ,
   @4='1988:06:25' 
 WHERE
   @1=10001 AND
   @2=20000 AND
   @3='1987:06:26' AND
   @4='1988:06:25' ;[root@test2 log]# sed -i 's/@1/emp_no/g;s/@2/salary/g;s/@3/from_date/g;s/@4/to_date/g' rollback.sql --这一步是把@1、@2、@3等替换成列名
 [root@test2 log]# more rollback.sql 
 UPDATE `employees`.`test`
 SET
   emp_no=10001 ,
   salary=60117 ,
   from_date='1986:06:26' ,
   to_date='1987:06:26' 
 WHERE
   emp_no=10001 AND
   salary=20000 AND
   from_date='1986:06:26' AND
   to_date='1987:06:26' ;
 UPDATE `employees`.`test`
 SET
   emp_no=10001 ,
   salary=62102 ,
   from_date='1987:06:26' ,
   to_date='1988:06:25' 
 WHERE
   emp_no=10001 AND
   salary=20000 AND

 7、解锁表  

mysql> unlock tables;
 Query OK, 0 rows affected (0.00 sec)

8、执行回滚语句  

mysql> source /data2/3301/log/rollback.sql
  Query OK, 0 rows affected (0.01 sec)
 Rows matched: 0  Changed: 0  Warnings: 0Query OK, 0 rows affected (0.01 sec)
 Rows matched: 0  Changed: 0  Warnings: 0Query OK, 0 rows affected (0.01 sec)
 Rows matched: 0  Changed: 0  Warnings: 0Query OK, 0 rows affected (0.01 sec)
 Rows matched: 0  Changed: 0  Warnings: 0Query OK, 0 rows affected (0.01 sec)
 Rows matched: 0  Changed: 0  Warnings: 0Query OK, 0 rows affected (0.01 sec)
 Rows matched: 0  Changed: 0  Warnings: 0


。。。。。

        上述方法比较适合对表的一条或几条数据的误更新,如果是全表的误更新,更新后没有其他的更新,也合适。对于全表的误更新,误更新后还有其他的更新,数量较少还能勉强恢复。如果误更新后有多次其他的更新操作,或者切换了binlog日志,这种方法就不适合了,只能使用binlog2sql工具

三、基于binlog2sql的误更新恢复

1、安装binlog2mysql
1)该工具的使用依赖以下三个包:
                PyMySQL==0.7.8
                wheel==0.24.0
                mysql-replication==0.9
        其中,每个包又会依赖其它包,所以安装这些包是一个比较麻烦的事情。
2)外网环境安装
        可直接通过pip install安装,它会自动下载并安装依赖包的。
        2.1.1 安装binlog2sql前先安装git和pip:
                yum -y install epel-release
                yum -y install git  python-pip
        2.1.2 安装binlog2sql:
                git clone https://github.com/danfengcao/binlog2sql.git && cd binlog2sql
                pip install -r requirements.txt
3)内网环境安装
        可手动安装这些包,目前,这些包已下载打包,并上传到百度云盘中,大家可自行下载。
        链接:https://pan.baidu.com/s/1lC3tEB27JSWAL3-ukr19oA 
        提取码:ange
        安装步骤:
                # tar xvf binlog2sql.tar.gz
                # cd binlog2sql/binlog2sql_dependencies/
                # tar xvf setuptools-0.6c11.tar.gz
                # cd setuptools-0.6c11
                # python setup.py install
                # cd ..
                # tar xvf pip-9.0.1.tar.gz
                # cd pip-9.0.1
                # python setup.py install
                # cd ..
                # pip install *.whl mysql-replication-0.9.tar.gz   
4)安装遇到的问题

(1)权限问题,如果mysql是用root用户安装的,那么安装这些包使用root用户不会有问题,如果用mysql安装,则需要给mysql用户sudo的权限。
(2)mysql-replication包的版本问题,我的环境是mysql8.0 使用mysql-replication-0.9的版本已经不兼容了,于是下载了最新的mysql-replication。下载地址:https://pypi.org/project/mysql-replication/#files

2、模拟误删除

mysql> update test set salary=20000;    --误更新
 Query OK, 500 rows affected (0.04 sec)
 Rows matched: 500  Changed: 500  Warnings: 0mysql> update test set salary=salary+100;    --误更新后的其他更新
 Query OK, 500 rows affected (0.01 sec)
 Rows matched: 500  Changed: 500  Warnings: 0mysql> show master status;
 +-----------------+----------+--------------+------------------+------------------------------------------+
 | File            | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
 +-----------------+----------+--------------+------------------+------------------------------------------+
 | mysqlbin.000002 |    30854 |              |                  | d83239cd-ce27-11ec-b08e-080027e2598e:1-4 |
 +-----------------+----------+--------------+------------------+------------------------------------------+
 1 row in set (0.00 sec)mysql> flush logs;    
 Query OK, 0 rows affected (0.06 sec)mysql> flush logs;    --切换了两次binlog
 Query OK, 0 rows affected (0.03 sec)mysql> show master status;
 +-----------------+----------+--------------+------------------+------------------------------------------+
 | File            | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
 +-----------------+----------+--------------+------------------+------------------------------------------+
 | mysqlbin.000004 |      196 |              |                  | d83239cd-ce27-11ec-b08e-080027e2598e:1-4 |
 +-----------------+----------+--------------+------------------+------------------------------------------+
 1 row in set (0.00 sec)mysql>

3、生成恢复语句

建立恢复用户,并赋予权限 

create  user  binlog2sql@'%' identified by 'binlog2sql';
 create  user  binlog2sql@'localhost' identified by 'binlog2sql';
 GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO  binlog2sql@'%';
 GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO  binlog2sql@'localhost';
 flush privileges;python binlog2sql.py -ubinlog2sql  -pbinlog2sql  -ddatabase_name   -t   table1 table2  --start-file='mysql-bin.000007' 
 --start-datetime='2019-12-xx 06:00:00' --stop-datetime='2019-12-xx 11:30:00'  >/tmp/binlog.txt    python binlog2sql.py  --flashback -h127.0.0.1 -P 3306   -ubinlog2sql -pbinlog2sql -ddatabase_name -t table1 table2 
 --start-file='mysql-bin.000007' --start-position=763 --stop-position=1147 >/tmp/rollback.txtbinlog2sql有很多参数可以选。可以基于时间筛选、可以基于binlog文件筛选、可以基于postition筛选等等。
 cd /soft/binlog2sql/binlog2sql
 生成redo语句
 python binlog2sql.py -ubinlog2sql  -pbinlog2sql  -demployees   -ttest --start-file='mysqlbin.000002' --stop-file='mysqlbin.000004' >/data2/3301/log/binlog.txt   
 生成回滚语句
 python binlog2sql.py  --flashback -h127.0.0.1 -P3306   -ubinlog2sql -pbinlog2sql -demployees   -ttest --start-file='mysqlbin.000002' --stop-file='mysqlbin.000004' >/data2/3301/log/rollback3.sql 生成成回滚语句时还遇到下面的错误:
 :
   File "binlog2sql.py", line 124, in <module>
     only_tables=args.tables, nopk=args.nopk, flashback=args.flashback, stopnever=args.stopnever)
   File "binlog2sql.py", line 38, in __init__
     self.connection = pymysql.connect(**self.connectionSettings)
   File "/usr/lib/python2.7/site-packages/pymysql/__init__.py", line 90, in Connect
     return Connection(*args, **kwargs)
   File "/usr/lib/python2.7/site-packages/pymysql/connections.py", line 690, in __init__
     self.connect()
   File "/usr/lib/python2.7/site-packages/pymysql/connections.py", line 939, in connect
     raise exc
 pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1' ([Errno 111] Connection refused)")

是因为binlog2mysql默认的端口是3306,如果mysql使用了非默认端口,需要特殊指定

注意,执行binlog2sql时会访问msyql的数据字典,因此数据库必须是运行状态 

4、执行恢复语句 

mysql> source /data2/3301/log/rollback3.sql