通过解析binlog,实现MySQL的flashback功能【python实现】

介绍了一种通过解析MySQL的二进制日志(binlog)来实现数据回滚的方法。该方法首先利用mysqlbinlog命令将binlog转换为文本格式,接着通过Python脚本进一步解析这些文本,生成易于理解的SQL语句并存储到数据库中,以便于后续的数据筛选与回滚操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

工作中经常会遇到需要确定MySQL库中在某个时间点修改了什么数据的需求,通过mysqlbinlog命令可以将binlog解析成文本,不过字段会显示成字段位置,如@1,可读性很差。

本脚本通过调用mysqlbinlog可执行文件,先将binlog解析成文本,然后通过python去解析文本,并替换成可读性较强的SQL语句,最后按每事务对应一条记录,存入数据库中,以方便筛选。回滚时,只需要过滤掉不相关的表、时间点,然后按log position倒序排序,即可得到正确的回滚语句。

经测试,文本解析速度为10276行/秒,每个事务大约对应60行文件(取决于你表的宽度),对应171.5事务/秒。

这种将二进制日志解析成文本再将文本解析成最终结果的方式效率不高,最好的方式应该是直接解析二进制日志。不过近期工作较忙,等有空再去学习二进制日志的格式吧,先凑和着用一段时间。

使用方法:

./flashback_mysql  mysql-binlog-path  -S='2016-05-16T18:00:00' -E='2016-05-16T19:00:00'  -h=192.168.88.11  -P=3306  -u=root  -p=xxx

下图是解析入库之后的结果:


以下是表结构:

CREATE TABLE `t_binlog_event` (
	`auto_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`binlog_name` VARCHAR(100) NOT NULL,
	`dml_start_time` DATETIME NOT NULL,
	`dml_end_time` DATETIME NOT NULL,
	`start_log_pos` BIGINT(20) NOT NULL,
	`end_log_pos` BIGINT(20) NOT NULL,
	`db_name` VARCHAR(100) NOT NULL,
	`table_name` VARCHAR(200) NOT NULL,
	`dml_sql` LONGTEXT NULL,
	`undo_sql` LONGTEXT NULL,
	PRIMARY KEY (`auto_id`),
	INDEX `dml_start_time` (`dml_start_time`),
	INDEX `dml_end_time` (`dml_end_time`),
	INDEX `start_log_pos` (`start_log_pos`),
	INDEX `end_log_pos` (`end_log_pos`),
	INDEX `db_name` (`db_name`),
	INDEX `table_name` (`table_name`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB


以下是源码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#参考代码:http://blog.youkuaiyun.com/selectdb/article/details/16861063
#解析出来的每个事务对应的DB名、表名、事务开始时间、结束时间、开始log pos、结束log pos保存在参数指定的MySQL实例的test.t_binlog_event中

import MySQLdb
import re
import os
import sys
import datetime

usage = """\nflashback_mysql 1.0 for python 2.6+
Usage:
    ./flashback_mysql  mysql-binlog-path  -S='2016-05-16T18:00:00' -E='2016-05-16T19:00:00'  -h=192.168.88.11  -P=3306  -u=root  -p=xxx
    Argv1 is mandatory, and must be set as mysql-binlog-path,
    The other argvs are optional, '-S' indicates '--start-datetime', '-E' indicates '--stop-datetime'
    tag:
        1. need MySQLdb module
        2. need your mysql server desc table privileges
        3. results will be stored in test.t_binlog_event on the MySQL instance you provided.
"""

class ClassFlashback:
    def __init_db(self):
        self.mysql_db = MySQLdb.connect(host=self.host, user=self.user, passwd=self.password, port=self.port, charset='utf8')
        self.mysql_db.autocommit(True)
        self.mysql_cur = self.mysql_db.cursor()

    def __init__(self):
        self.host = ''
        self.user = ''
        self.password = ''
        self.port = ''
        self.db = ''
        self.start_datetime = ''
        self.stop_datetime = ''
        self.tmp_binlog_file = 'mysqlbin000125.txt'
        self.field = []
        self.db_name = ''
        self.tb_name = ''
        self.patt = re.compile("/* .* */")
        self._get_argv()
        self.__init_db()
        self.begin_time = ''
        self.end_time = ''
        self.start_pos = ''
        self.end_pos = ''
        self.dml_sql = ''
        self.undo_sql = ''

    def _get_argv(self):
        if len(sys.argv) == 1:
            print usage
            sys.exit(1)
        elif sys.argv[1] == '--help' or sys.argv[1] == '-h':
                print usage
                sys.exit()
        elif len(sys.argv) > 2:
            for i in sys.argv[2:]:
                _argv = i.split('=')
                if _argv[0] == '-S':  self.start_datetime = _argv[1].replace('T', ' ')
                elif _argv[0] == '-E':  self.stop_datetime = _argv[1].replace('T', ' ')
                # elif _argv[0] == '-d':  self.db = _argv[1]
                elif _argv[0] == '-h':  self.host = '%s' % _argv[1]
                elif _argv[0] == '-P':  self.port = int('%s' % _argv[1])
                elif _argv[0] == '-u':  self.user = '%s' % _argv[1]
                elif _argv[0] == '-p':
                    if len(_argv) == 2: self.password = '%s' % _argv[1]
                    elif len(_argv) == 1:   self.password = raw_input('Please enter your mysql passwd: ')
                else:   print usage; sys.exit(1)
        self.input_binlog_file = sys.argv[1]
        if self.port == '': self.port = 3306
        elif self.password == '': self.password = raw_input('Please enter your mysql passwd: ')
        elif self.start_datetime != '' and self.stop_datetime != '':
            self.start_datetime = "--start-datetime='" + self.start_datetime + "'"
            self.stop_datetime = "--stop-datetime='" + self.stop_datetime + "'"
        elif self.start_datetime != '':
            self.start_datetime = "--start-datetime='" + self.start_datetime + "'"
        elif self.stop_datetime != '':
            self.stop_datetime = "--stop-datetime='" + self.stop_datetime + "'"

    def _create_tab(self):
        create_tb_sql = """
        CREATE TABLE IF NOT EXISTS test.t_binlog_event (
            auto_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
            binlog_name VARCHAR(100) NOT NULL ,
            dml_start_time DATETIME NOT NULL,
            dml_end_time DATETIME NOT NULL,
            start_log_pos BIGINT NOT NULL,
            end_log_pos BIGINT NOT NULL,
            db_name VARCHAR(100) NOT NULL ,
            table_name VARCHAR(200) NOT NULL ,
            dml_sql LONGTEXT NULL ,
            undo_sql LONGTEXT NULL ,
            PRIMARY KEY (auto_id),
            INDEX dml_start_time (dml_start_time),
            INDEX dml_end_time (dml_end_time),
            INDEX start_log_pos (start_log_pos),
            INDEX end_log_pos (end_log_pos),
            INDEX db_name (db_name),
            INDEX table_name (table_name)
        )
        COLLATE='utf8_general_ci' ENGINE=InnoDB;
        """
        self.mysql_cur.execute(create_tb_sql)

    def _release_db(self):
        self.mysql_cur.close()
        self.mysql_db.close()

    def _get_table_name(self,line):
        try:
            if line.find('Table_map:') != -1:
                l = line.index('server')
                m = line.index('end_log_pos')
                n = line.index('Table_map')
                begin_time = line[:l:].rstrip(' ').replace('#', '20')
                self.begin_time = begin_time[0:4] + '-' + begin_time[4:6] + '-' + begin_time[6:]
                self.start_pos = int(line[m::].split(' ')[1])
                self.db_name = line[n::].split(' ')[1].replace('`', '').split('.')[0]
                self.tb_name = line[n::].split(' ')[1].replace('`', '').split('.')[1]
        except Exception, ex:
            print ex

    def _get_end_time(self,line):
        try:
            if line.find('Xid =') != -1:
                l = line.index('server')
                m = line.index('end_log_pos')
                end_time = line[:l:].rstrip(' ').replace('#', '20')
                self.end_time = end_time[0:4] + '-' + end_time[4:6] + '-' + end_time[6:]
                self.end_pos = int(line[m::].split(' ')[1])

                self.dml_sql = self.dml_sql.replace("'", "''''") + ';'
                if self.dml_sql.find('INSERT INTO ') != -1:
                    self.undo_sql = self.dml_sql.replace('INSERT INTO', 'DELETE FROM').replace('SET', 'WHERE')
                elif self.dml_sql.find('UPDATE ') != -1:
                    self.undo_sql = self.dml_sql.replace('WHERE', 'WHERETOxxx').replace('SET', 'WHERE').replace('WHERETOxxx', 'SET')
                elif self.dml_sql.find('DELETE ') != -1:
                    self.undo_sql = self.dml_sql.replace('DELETE FROM', 'INSERT INTO').replace('WHERE', 'SET')
                # print self.begin_time, self.end_time, self.start_pos, self.end_pos, self.db_name, self.tb_name, self.dml_sql
                # print self.undo_sql + '\n\n\n'
                insert_sql = "insert into test.t_binlog_event values (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (self.input_binlog_file, self.begin_time, self.end_time, self.start_pos, self.end_pos, self.db_name, self.tb_name, self.dml_sql, self.undo_sql)
                # print insert_sql
                self.mysql_cur.execute(insert_sql)
                self.dml_sql = ''
                self.undo_sql = ''
                #此处一个事务结束, 将self.dml_sql 和self.undo_sql置空, 用于存放下一条解析出来的DML语句
        except Exception, ex:
            print ex

    def _get_table_structure(self, db_name, tb_name):
        desc_sql = 'desc %s.%s' % (db_name, tb_name)
        self.field = []
        self.mysql_cur.execute(desc_sql)
        res = self.mysql_cur.fetchall()
        for j in res:
            self.field.append(j[0])

    def _do(self):
        '''先把mysql二进制的binlog解析成可识别文件,再从里面提取需要的数据'''

        starttime = datetime.datetime.now()
        print ("\nConverting binlog to text file...")
        # os.popen('mysqlbinlog --no-defaults -v -v --base64-output=DECODE-ROWS %s %s %s > %s' %(self.start_datetime, self.stop_datetime, self.input_binlog_file,self.tmp_binlog_file))
        print "mysqlbinlog --no-defaults -v -v --base64-output=DECODE-ROWS %s %s %s > %s" %(self.start_datetime, self.stop_datetime, self.input_binlog_file,self.tmp_binlog_file)
        print ("File converting complete.")
        endtime = datetime.datetime.now()
        timeinterval = endtime - starttime
        print("Converting elapsed :" + str(timeinterval.seconds) + '.' + str(timeinterval.microseconds) + " seconds")

        self._create_tab()
        print ("\nParsing file...")
        starttime = datetime.datetime.now()
        with open(self.tmp_binlog_file, "r") as infile:
            for line in infile.readlines():
                if line.find('Table_map:') != -1: #匹配到表名,解析开始时间、开始log pos、DB名、表名,并获取表结构
                    self._get_table_name(line)
                    self._get_table_structure(self.db_name, self.tb_name)
                    line = '' # 将这一行内容清空,否则line的内容将会传入到dml_sql中去
                elif line.find('###   @') != -1: #匹配到字段内容, 将字段位置代码替换成字段名,并去除字段值后面的字段类型等内容
                    i = line.replace('###   @', '').split('=')[0]
                    line = unicode(line, "utf-8")
                    line = line.replace('###   @'+str(i), self.field[int(i)-1])
                    # if(int(i) == len(self.field)):
                    #     line = self.patt.sub(' ', line)
                    # else:
                    line = self.patt.sub(',', line)
                elif line.find('###') != -1: #匹配到关键字,如UPDATE, WHERE, SET等等
                    line = line.replace('###', '')
                elif line.find('Xid =') != -1: #匹配到提交时间,到这里整个事务解析完毕
                    self._get_end_time(line)
                    line = '' # 将这一行内容清空,否则line的内容将会传入到dml_sql中去
                else:
                    line = ''  # 丢弃其他信息

                if line.rstrip('\n') != '':
                    #到此处只会是SQL语句
                    self.dml_sql = self.dml_sql + line + ' '

        print ("\nParsing completed.")
        endtime = datetime.datetime.now()
        timeinterval = endtime - starttime
        print("Parsing elapsed :" + str(timeinterval.seconds) + '.' + str(timeinterval.microseconds) + " seconds")

def main():
    p = ClassFlashback()
    p._do()
    p._release_db()

if __name__ == "__main__":
    main()


<think>好的,我现在需要帮助用户解析MySQLbinlog文件,他们可能是为了查看变更记录或进行数据恢复。首先,我得回忆一下binlog的基本知识。 用户的问题是关于如何解析binlog,包括文件格式和工具。根据提供的引用内容,有几个工具可用,比如mysqlbinlogbinlog2sql、canal等。我需要先解释binlog的三种格式:STATEMENT、ROW和MIXED,因为不同的格式会影响解析的方式和输出内容。比如ROW格式会记录行数据的变化,而STATEMENT记录SQL语句。 接下来是工具部分。系统自带的mysqlbinlog是最基础的,用户可能需要知道基本命令和如何转换成可读的SQL。引用1和2都提到了这个工具,应该详细说明使用示例,比如如何指定时间或位置点,以及输出到文件。然后,第三方工具如binlog2sql,引用3和4提到它可以直接生成回滚SQL,这对数据恢复很有用,需要介绍安装和使用步骤,确保用户知道权限和版本兼容性问题。还有canal,引用5提到它是阿里开源的,用于实时解析,适合同步或监控场景,可能需要配置MySQL权限和canal的实例配置。 用户可能需要步骤化的指导,比如先确认binlog格式,再选择合适的工具。同时,要提醒注意事项,比如权限、数据敏感性和版本差异。此外,生成相关问题的时候,要考虑他们可能的后续需求,比如如何恢复数据、处理大事务或安全问题。 需要检查是否有遗漏的工具或步骤,比如Percona Toolkit在引用2中提到,但用户没有提供详细信息,可能暂时不提。确保回答结构清晰,分点列出格式和工具,每个工具的使用方法,并给出示例代码块,符合用户的格式要求。最后,相关问题要引导用户深入,比如优化解析效率或数据恢复步骤。</think>### MySQL binlog文件解析方法及工具说明 #### 一、binlog文件格式解析 MySQL binlog文件包含以下三种格式: 1. **STATEMENT** 记录实际执行的SQL语句,格式为逻辑日志,例如: $$ \text{UPDATE users SET name='Alice' WHERE id=1} $$ 优点是日志体积小,但可能因函数或触发器导致主从不一致[^2]。 2. **ROW** 记录数据行的变更(默认格式),例如: $$ \text{id=1, name: 'Bob' → 'Alice'} $$ 支持精确的数据恢复,但日志体积较大[^5]。 3. **MIXED** 混合模式,优先使用STATEMENT,特定场景自动切换为ROW[^2]。 --- #### 二、解析工具及使用方法 ##### 1. 官方工具`mysqlbinlog` ```bash # 解析为可读SQL(ROW格式需添加参数) mysqlbinlog --base64-output=decode-rows -v /var/lib/mysql/binlog.000001 # 按时间范围解析 mysqlbinlog --start-datetime="2024-01-01 00:00:00" --stop-datetime="2024-01-02 00:00:00" binlog.000001 # 输出到文件 mysqlbinlog binlog.000001 > output.sql ``` *适用场景*:快速查看变更记录或生成SQL备份[^1][^2]。 ##### 2. 第三方工具`binlog2sql` ```bash # 安装 pip install binlog2sql # 生成回滚SQL(误操作恢复) python binlog2sql.py -h127.0.0.1 -uadmin -p'password' \ --start-file='binlog.000001' --start-pos=1234 --end-pos=5678 \ --flashback ``` *特点*:支持生成正向/反向SQL,直接用于数据恢复[^3][^4]。 ##### 3. 阿里开源工具`canal` ```properties # 配置instance.properties(监听指定库) canal.instance.master.address=127.0.0.1:3306 canal.instance.dbUsername=canal canal.instance.dbPassword=canal canal.instance.filter.regex=test.user ``` *功能*:实时解析binlog并推送至Kafka/RocketMQ,适用于数据同步场景[^5]。 --- #### 三、操作步骤建议 1. **确认binlog格式** ```sql SHOW VARIABLES LIKE 'binlog_format'; ``` 2. **选择工具** - 简单查看:`mysqlbinlog` - 数据恢复:`binlog2sql` - 实时订阅:`canal` 3. **注意事项** - ROW格式需添加`-v`参数解析内容 - 确保账号有`REPLICATION CLIENT`权限[^5] - 敏感数据需脱敏处理[^3] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值