当前位置:Gxlcms > 数据库问题 > mysql基于binlog回滚工具_flashback(python版本)

mysql基于binlog回滚工具_flashback(python版本)

时间:2021-07-01 10:21:17 帮助过:12人阅读

1 实现内容

    根据binlog文件,对某个\些事务、某段时间某些表、某段时间全库 做回滚操作,实现闪回功能。工具处理过程中,会把binlog中的事务修改的行记录存储到表格中去,通过 dml_sql 列,可以查看每一个事务内部的所有行记录变更情况,通过 undo_sql 查看回滚的SQL内容。如下图,然后再根据表格内容做回滚操作。      技术分享       那么这个脚本有哪些优点呢?
  • 回滚分为2个命令:第一个命令 分析binglog并存储进入数据库;第二个命令 执行回滚操作;
  • 回滚的时候,可以把执行脚本跟回滚脚本统一存放到数据库中,可以查看 更新内容以及回滚内容;
  • 根据存储的分析表格,方便指定事务或者指定表格来来恢复;
  • 详细的日志输出,说明分析进度跟执行进度。
    分析binlog的输出截图(分析1G的binlog文件)     技术分享      回滚数据库的输出截图:    技术分享

2 原理

    前提:实例启动了binlog并且格式为ROW。          使用python对mysqlbinlog后的log文件进行文本分析处理,在整个处理过程中,有以下6个疑难点需要处理:
  1. 判断一个事务的开始跟结束
  2. 同一个事务的执行顺序需要反序执行
  3. 解析回滚SQL
  4. 同一个事务操作不同表格处理
  5. 转义字符处理,比如换行符、tab符等等
  6. timestamp数据类型参数值转换
  7. 负数处理
  8. 单个事务涉及到行修改SQL操作了 max_allow
  9. 针对某个表格做回滚,而不是全库回滚
    

2.1 事务的开始与结束

    按照Xid出现的位置来判断,从binlog文件的最开始开始读取,遇到SQL语句则提取出来,直到遇到Xid,统一把之前提取出来的SQL汇总为一个事务,然后继续提取SQL语句,直到遇到下一个Xid,再把这个事务的SQL汇总成一个事务,一直这样循环,直至文件顺序遍历结束。     技术分享

 

2.2 事务内部反序处理

    同一个事务中,如果有多个表格多行记录发生变更,在回滚的时候,应该反序回滚SQL,那么,如何将提取出来的SQL反序存储呢?思路如下:
  • 每行记录的修改SQL独立出来
  • 将独立出来的SQL反序存储
   假设正序的事务SQL语句存储在变量 dml_sql 中,反序后的可以回滚的SQL存储在变量 undo_sql中。按顺序把行记录修改的SQL抽取出来 存储到变量 record_sql 中去,然后 赋值 undo_sql =record_sql + undo_sql ,再置空 record_sql 变量,如此,便可实现反序事务内部的执行SQL。

2.3 解析回滚SQL

    首先,查看binlog的日志内容,发现行修改的SQL情形如下,提取过程中需要注意这几个问题:
  • 行记录的列名配对,binlog file存储的列序号,不能直接使用
  • WHERE部分 跟 SET部分 之间并无关键字或者符号,需要添加 AND 或者 逗号
  • DELETE SQL 需要反转为 INSERT
  • UPDATE SQL 需要把WHERE 跟 SET的部分进行替换
  • INSERT SQL需要反转为 DELETE
  • 技术分享

2.4 同事务不同表格处理

    同一个事务中,允许对不同表格进行数据修改,这点在列名替换列序号的时候,需要留意处理。     每一个的行记录前有一行记录,含有 ‘Table_map‘ 标识,会说明这一行当行记录是修改哪个表格,可以根据这个提示,来替换binlog里边的列序号为列名。

2.5 转义字符处理

    binlog文件在对非空格的空白字符处理,采用转义字符字符串存储,比如,在表格insert一列记录含换行符,而实际上在binlog文件中,是使用了 \x0a 替换了 换行操作,所以在回滚数据的过程中,需要对转义字符做处理。     技术分享

    技术分享

 

         这里注意一个地方,039的转义字符是没有在函数 esc_code 中统一处理,而是单独做另外处理。     技术分享

 

    转移字符表相见下图:     技术分享

 

2.6 timestamp数据类型处理

    timestamp实际在数据库中的存储值是 INT类型,需要使用 函数 from_unixtime转换。     建立测试表格tbtest,只有一列timestamp的列,存储值后查看binlog的内容,具体截图如下:     技术分享

 

    在处理行记录的时候,要对timestamp的value做处理,添加from_unixtime函数转换。

2.7 负数值处理

    这个一开始写代码的时候,并没有考虑到。大量测试的过程中发现,所有整型的数据类型,在存储负数的时候,都会存入一个最大范围值。binlog在处理这块的机制有些不是很了解。测试如下:     技术分享

 

   所以当遇到INT的各种数据类型并且VALUE为负数的时候,需要把 这个范围值去除,才能执行执行undo_sql。

2.8 单个事务行记录总SQL超过max_allowed_package处理

    分析binlog后存储两种sql类型,一种是行记录的修改SQL,即 dml_sql;一种是 行记录的回滚sql,即 undo_sql。从代码可知,存储这两个sql的列是longtext,最大可存储4G的内容。但是 MySQL中单个会话的包大小是有限制的,限制的参数为 max_allowed_packet,默认大小为 4Mb,最大为1G,所以这个脚本使用前,请手动设置 存储binlog file的数据库实例以及线上的数据库实例这个参数: set global max_allowed_packet = 1073741824; #记得后续修改回来          万一操作了呢?那么回滚只能分段来回滚,先回滚到 这个大事务,然后单独执行这个大事务,紧接着继续回滚,这部分不能使用pymysql嗲用source 文件执行,所以只能手动做这个操作。 求高能人士修改这个逻辑代码!!!

2.9 针对性回滚

    假设误操作的没有明确的时间点,只有一个区间,而这个区间还有其他的表格操作,那么这个时候,需要在分析binlog文件的时候,添加--database选项,先帅选到同一个数据库中binlog文件中。     这里的处理是将这段区间的dml_sql跟undo_sql都存储到数据库表格中,然后再删除不需要回滚的事务,剩余需要回滚的事务。再执行回滚操作。

3 使用说明

3.1 参数说明

    这个脚本的参数稍微多些,可以 --help 查看具体说明。     技术分享

 

    本人喜欢用各种颜色来分类参数(blingbling五颜六色,看着多有趣多精神),所以,按颜色来说明这些参数。
  • 黄色区域:这6个参数,提供的是 分析并存储binlog file的相关值,说明存储分析结果的数据库的链接方式、binlog文件的位置以及存储结果的表格名字;
  • 蓝色区域:这4个参数,提供 与线上数据库表结构一致的DB实例连接方式,仅需跟线上一模一样的表结构,不一定需要是主从库;
  • 绿色区域:最最重要的选项 -a,0代表仅分析binlog文件,1代表仅执行回滚操作,必须先执行0才可以执行1;
  • 紫色区域:举例说明。

3.2 应用场景说明

  • 全库回滚某段时间
    • 需要回滚某个时间段的所有SQL操作,回滚到某一个时间点
    • 这种情况下呢,大多数是使用备份文件+binlog解决
    • 但是这个脚本也可以满足,但请勿直接在线上操作,先 -a=0,看下分析结果,是否符合,符合的话,停掉某个从库,再在从库上执行,最后开发业务接入检查是否恢复到指定时间点,数据是否正常。
  • 某段时间某些表格回滚某些操作
    • 比如,开发提交了一个批量更新脚本,各个测试层面验证没有问题,提交线上执行,但是执行后,发现有个业务漏测试,导致某些字段更新后影响到其他业务,现在需要紧急 把被批量更新的表格回滚到原先的行记录
    • 这个并不能单纯从技术角度来处理,要综合考虑
      • 技术分享

         

      • 这种情况下,如何回顾tab A表格的修改操作呢?
      • 个人觉得,这种方式比较行得通,dump tabA表格的数据到测试环境,然后再分析 binlog file 从11点-12点的undo sql,接着在测试环境回滚该表格到11点这个时刻,紧接着,由开发跟业务对比测试环境11点的数据跟线上现有的数据中,看下是哪些行哪些列需要在线上进行回滚,哪些是不需要的,然后开发再提交SQL脚本,再在线上执行。其实,这里边,DBA仅提供一个角色,就是把 表格 tab A 在一个新的环境上,回滚到某个时间点,但是不提供直接线上回滚SQL的处理。
  • 回滚某个/些SQL
    • 这种情况比较常见,某个update某个delete缺少where条件或者where条件执行错误
    • 这种情况下,找到对应的事务,执行回滚即可,回滚流程请参考上面一说,对的,我就是这么胆小怕事 技术分享

       

3.3 测试案例

3.3.1 全库回滚某段时间

假设需要回滚9点10分到9点15分间数据库的所有操作:
  • 准备测试环境实例存储分析后的数据 
  • 测试环境修改set global max_allowed_packet = 1073741824
  • mysqlbinlog分析binlog文件
  • python脚本分析文件,action=0
  • 线上测试环境修改set global max_allowed_packet = 1073741824
  • 回滚数据,action=1
  • 线上测试环境修改set global max_allowed_packet = 4194304
 1 --测试环境(请安装pymysql):IP: 192.168.9.242,PORT:3310 ,数据库:flashback,表格:tbevent
 2 --具有线上表结构的db:IP:192.168.9.243 PORT:3310
 3 
 4 
 5 mysql> show global variables like max_allowed_packet;
 6 +--------------------+----------+
 7 | Variable_name      | Value    |
 8 +--------------------+----------+
 9 | max_allowed_packet | 16777216 |
10 +--------------------+----------+
11 1 row in set (0.00 sec)
12 
13 mysql> set global max_allowed_packet = 1073741824;
14 Query OK, 0 rows affected (0.00 sec)
15 
16 [root@sutest244 ~]# mysqlbinlog --start-datetime=‘2017-06-19 09:00:00‘ --stop-datetime=‘2017-06-19 10:00:00‘ --base64-output=decode-rows -v ~/data/mysql/data/mysql-bin.007335 > /tmp/binlog.log
17 
18 [root@sutest242 pycharm]# python su_flashback.py -h=127.0.0.1 -P=3310 -u=root -p=**** -f=/tmp/binlog.log -t=flashback.tbevent -oh=192.168.9.244 -oP=3310 -u=root -op=**** -a=0
19 2017-06-19 10:59:39,041 INFO begin to assign values to parameters
20 2017-06-19 10:59:39,041 INFO assign values to parameters is done:host=127.0.0.1,user=root,password=***,port=3310,fpath=/tmp/binlog.log,tbevent=flashback.tbevent
21 2017-06-19 10:59:39,049 INFO MySQL which userd to store binlog event connection is ok
22 2017-06-19 10:59:39,050 INFO assign values to online mysql parameters is done:host=192.168.9.244,user=,password=***,port=3310
23 2017-06-19 10:59:39,054 INFO MySQL which userd to analyse online table schema connection is ok
24 2017-06-19 10:59:39,054 INFO MySQL connection is ok
25 2017-06-19 10:59:39,055 INFO creating table flashback.tbevent to store binlog event
26 2017-06-19 10:59:39,058 INFO created table flashback.tbevent 
27 2017-06-19 10:59:39,060 INFO begining to analyze the binlog file ,this may be take a long time !!!
28 2017-06-19 10:59:39,061 INFO analyzing...
29 2017-06-19 11:49:53,781 INFO finished to analyze the binlog file !!!
30 2017-06-19 11:49:53,782 INFO release all db connections
31 2017-06-19 11:49:53,782 INFO All done,check the flashback.tbevent which stored binlog event on host 127.0.0.1 , port 3310 
32 
33 
34 [root@sutest242 pycharm]# python su_flashback.py -h=127.0.0.1 -P=3310 -u=root -p=**** -f=/tmp/binlog.log -t=flashback.tbevent -oh=192.168.9.244 -oP=3310 -u=root -op=**** -a=1
35 2017-06-19 16:30:20,633 INFO begin to assign values to parameters
36 2017-06-19 16:30:20,635 INFO assign values to parameters is done:host=127.0.0.1,user=root,password=***,port=3310,fpath=/tmp/binlog.log,tbevent=flashback.tbevent
37 2017-06-19 16:30:20,865 INFO MySQL which userd to store binlog event connection is ok
38 2017-06-19 16:30:20,866 INFO assign values to online mysql parameters is done:host=192.168.9.244,user=,password=***,port=3310
39 2017-06-19 16:30:20,871 INFO MySQL which userd to analyse online table schema connection is ok
40 2017-06-19 16:30:20,871 INFO MySQL connection is ok
41 2017-06-19 16:30:21,243 INFO There has 347868 transactions ,need 35 batchs ,each batche doing 10000 transactions 
42 2017-06-19 16:30:21,243 INFO doing batch : 1 
43 2017-06-19 16:31:01,182 INFO doing batch : 2 
44 2017-06-19 16:31:16,909 INFO doing batch : 3 
45 -------省空间忽略不截图--------------
46 2017-06-19 16:41:11,287 INFO doing batch : 34 
47 2017-06-19 16:41:25,577 INFO doing batch : 35 
48 2017-06-19 16:41:44,629 INFO release all db connections
49 2017-06-19 16:41:44,630 INFO All done,check the flashback.tbevent which stored binlog event on host 127.0.0.1 , port 3310

3.3.2 某段时间某些表格回滚某些操作

  • 准备测试环境实例存储分析后的数据 
  • 测试环境修改set global max_allowed_packet = 1073741824
  • mysqlbinlog分析binlog文件
  • python脚本分析文件,action=0
  • 分析帅选需要的事务,rename表格
  • dump 对应的表格到测试环境
  • 回滚数据,action=1
  • 提交给开发业务对比数据

3.3.3 回滚某个/些SQL

  • 准备测试环境实例存储分析后的数据 
  • 测试环境修改set global max_allowed_packet = 1073741824
  • mysqlbinlog分析binlog文件
  • python脚本分析文件,action=0
  • 分析帅选需要的事务,rename表格
  • dump 对应的表格到测试环境
  • 回滚数据,action=1
  • 提交给开发业务对比数据

4 python脚本

     脚本会不定期修复bug,若是感兴趣,可以往github下载:https://github.com/xinysu/mysql.git 中的 mysql_xinysu_flashback 。

  1 # -*- coding: utf-8 -*-
  2 __author__ = xinysu
  3 __date__ = 2017/6/15 10:30
  4 
  5 
  6 
  7 import re
  8 import os
  9 import sys
 10 import datetime
 11 import time
 12 import logging
 13 import importlib
 14 importlib.reload(logging)
 15 logging.basicConfig(level=logging.DEBUG,format=%(asctime)s %(levelname)s %(message)s )
 16 
 17 import pymysql
 18 from pymysql.cursors import DictCursor
 19 
 20 usage=‘‘‘\nusage: python [script‘s path] [option]
 21 ALL options need to assign:
 22 \033[1;33;40m
 23 -h    : host, the database host,which database will store the results after analysis
 24 -u    : user, the db user
 25 -p    : password, the db user‘s password
 26 -P    : port, the db port
 27 
 28 -f    : file path, the binlog file
 29 -t    : table name, the table name to store the results after analysis , {dbname}.{tbname},
 30         when you want to store in `test` db and the table name is `tbevent`,then this parameter 
 31         is test.tbevent
 32 \033[1;34;40m
 33 -oh   : online host, the database host,which database have the online table schema
 34 -ou   : online user, the db user
 35 -op   : online password, the db user‘s password
 36 -oP   : online port, the db port
 37 \033[1;32;40m
 38 -a    : action, 
 39         0 just analyse the binlog file ,and store sql in table; 
 40         1 after execute self.dotype=0, execute the undo_sql in the table
 41 \033[0m  
 42 --help: help document
 43 \033[1;35;40m
 44 Example:
 45 analysize binlog:
 46 python su_flashback.py -h=127.0.0.1 -P=3310 -u=root -p=*** -f=/tmp/binlog.log -t=flashback.tbevent 
 47                        -oh=192.168.9.244 -oP=3310 -u=root -op=*** 
 48                        -a=0
 49 
 50 flash back:
 51 python su_flashback.py -h=127.0.0.1 -P=3310 -u=root -p=*** -f=/tmp/binlog.log -t=flashback.tbevent 
 52                        -oh=192.168.9.244 -oP=3310 -u=root -op=*** 
 53                        -a=1
 54 \033[0m                        
 55 ‘‘‘
 56 
 57 class flashback:
 58     def __init__(self):
 59         self.host=‘‘
 60         self.user=‘‘
 61         self.password=‘‘
 62         self.port=3306
 63         self.fpath=‘‘
 64         self.tbevent=‘‘
 65 
 66         self.on_host=‘‘
 67         self.on_user=‘‘
 68         self.on_password=‘‘
 69         self.on_port=3306
 70 
 71         self.action=0 # 0 just analyse the binlog file ,and store sql in table;1 after execute self.dotype=0, execute the undo_sql in the table
 72 
 73         self._get_db() # 从输入参数获取连接数据库的相关参数值
 74 
 75         # 连接数据库,该数据库是用来存储binlog文件分析后的内容
 76         logging.info(assign values to parameters is done:host={},user={},password=***,port={},fpath={},tbevent={}.format(self.host,self.user,self.port,self.fpath,self.tbevent))
 77         self.mysqlconn = pymysql.connect(host=self.host, user=self.user, password=self.password, port=self.port,charset=utf8)
 78         self.cur = self.mysqlconn.cursor(cursor=DictCursor)
 79         logging.info(MySQL which userd to store binlog event connection is ok)
 80 
 81         # 连接数据库,该数据库的表结构必须跟binlogfile基于对数据库表结构一致
 82         # 该数据库用于提供 binlog file 文件中涉及到表结构分析
 83         logging.info(assign values to online mysql parameters is done:host={},user={},password=***,port={}.format(self.on_host, self.on_user, self.on_port))
 84         self.on_mysqlconn = pymysql.connect(host=self.on_host, user=self.on_user, password=self.on_password, port=self.on_port,charset=utf8)
 85         self.on_cur = self.on_mysqlconn.cursor(cursor=DictCursor)
 86         logging.info(MySQL which userd to analyse online table schema connection is ok)
 87 
 88         logging.info(\033[33mMySQL connection is ok\033[0m)
 89 
 90         self.dml_sql=‘‘
 91         self.undo_sql=‘‘
 92 
 93         self.tbfield_where = []
 94         self.tbfield_set = []
 95 
 96         self.begin_time=‘‘
 97         self.db_name=‘‘
 98         self.tb_name=‘‘
 99         self.end_time=‘‘
100         self.end_pos=‘‘
101         self.sqltype=0
102 
103     #_get_db用于获取执行命令的输入参数
104     def _get_db(self):
105         logging.info(begin to assign values to parameters)
106         if len(sys.argv) == 1:
107             print(usage)
108             sys.exit(1)
109         elif sys.argv[1] == --help:
110             print(usage)
111             sys.exit()
112         elif len(sys.argv) > 2:
113             for i in sys.argv[1:]:
114                 _argv = i.split(=)
115                 if _argv[0] == -h:
116                     self.host = _argv[1]
117                 elif _argv[0] == -u:
118                     self.user = _argv[1]
119                 elif _argv[0] == -P:
120                     self.port = int(_argv[1])
121                 elif _argv[0] == -f:
122                     self.fpath = _argv[1]
123                 elif _argv[0] == -t:
124                     self.tbevent = _argv[1]
125                 elif _argv[0] == -p:
126                     self.password = _argv[1]
127 
128                 elif _argv[0] == -oh:
129                     self.on_host = _argv[1]
130                 elif _argv[0] == -ou:
131                     self.on_user = _argv[1]
132                 elif _argv[0] == -oP:
133                     self.on_port = int(_argv[1])
134                 elif _argv[0] == -op:
135                     self.on_password = _argv[1]
136 
137                 elif _argv[0] == -a:
138                     self.action = _argv[1]
139 
140                 else:
141                     print(usage)
142 
143     #创建表格,用于存储分析后的BINLOG内容
144     def create_tab(self):
145         logging.info(creating table {} to store binlog event.format(self.tbevent))
146         create_tb_sql =‘‘‘
147         CREATE TABLE IF NOT EXISTS {}(
148             auto_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
149             binlog_name VARCHAR(100) NOT NULL COMMENT ‘the binlog file path and name‘,
150             dml_start_time DATETIME NOT NULL COMMENT ‘when to start this transaction ‘,
151             dml_end_time DATETIME NOT NULL COMMENT ‘when to finish this transaction ‘,
152             end_log_pos BIGINT NOT NULL COMMENT ‘the log position for finish this transaction‘,
153             db_name VARCHAR(100) NOT NULL COMMENT ‘which database happened this transaction ‘,
154             table_name VARCHAR(200) NOT NULL COMMENT ‘which table happened this transaction ‘,
155             sqltype INT NOT NULL COMMENT ‘1 is insert,2 is update,3 is delete‘,
156             dml_sql LONGTEXT NULL  COMMENT ‘what sql excuted‘,
157             undo_sql LONGTEXT NULL COMMENT ‘rollback sql, this sql used for flashback‘,
158             PRIMARY KEY (auto_id),
159             INDEX sqltype(sqltype),
160             INDEX dml_start_time (dml_start_time),
161             INDEX dml_end_time (dml_end_time),
162             INDEX end_log_pos (end_log_pos),
163             INDEX db_name (db_name),
164             INDEX table_name (table_name)
165         )
166         COLLATE=‘utf8_general_ci‘ ENGINE=InnoDB;

                        
                    

人气教程排行