1. 库表级闪回与记录级闪回

mysqlbinlog 库表级 flashback 特性由同事 王翔 引入并实现。库表级 flashback 实现:

借鉴MariaDB Flashback实现思路,我们在TenDB3(MySQL 5.7)版本中实现了Flashback。
binlog文件由多个event组成: 如ALTER、DROP等语句在binlog中以Query_event记录,Write_rows_event表示ROW模式下插入一行数据;
通过"三层反转"实现Flashback:

  1. 单行数据反转: 也就是单个event反转(insert反转为delete,delete反转为insert,update交换set和where部分);
  2. 单个更新sql反转: 一个sql可能更新了多行数据,则需要将单个sql更新的行更新顺序反转(第一行反转为最后一行,最后一行反转为第一行);
  3. 整体sql反转: 全部binlog在整体时间上反转(最开始更新的sql,反转后,最后执行;最后更新的sql,反转后,最先执行)

腾讯互娱 CROS DBA 在库表级 flashback 的基础上,实现了记录级过滤和flashback。

在使用 mysqlbinlog 解析 row 格式的 binlog 文件时指定 --filter-rows 过滤条件,输出满足条件的 row_event,达到获取单记录变更流水的目的。配合 --flashback --flashback-tables 选项可以将这条 binlog row 逆转,达到单记录回档的目的。

据统计,单用户回档的需求,占总需求的50%(其它的回档类型有单库回档、全表回档,还有普通数据构造需求)

MySQL如何监控事务回滚耗时 mysql查看回滚记录_mysql

传统的单用户回档,只能构造出整个实例的数据,再滚动应用binlog。机器成本、时间成本都很高。
结合 mysql row 模式 和 这里的 mysqlbinlog 单用户过滤,可以直接操作 binlog 实现回档。

mysqlbinlog 新增特性:

  • 支持过滤指定表名的 row events
  • 支持根据 @2 aaa 的格式过滤 binlog 里面的记录,结果可直接应用
  • 指定 --flashback 选项,上述过滤相当于回档部分记录
  • 可以指定如何处理 query events,报错、忽略、保留等

使用注意:

  1. 如果要执行 flashback binlog 导入,可能需要配合幂等模式 slave_exec_mode=IDEMPOTENT 使用
  2. 同样,可能需要调大 max_allowed_packet
  3. 对于 int 类型,mysqlbinlog 的输出始终会输出一个 signed 和一个unsigned,因为 mysqlbinlog 不能准确知道字段的类型。所以在处理负数时,记得带:signed
  4. 建议是 binlog_row_image=FULL。如果是MINIMAL,给定的字段条件一定要在主键中
  5. flashback 闪回不支持ddl(包括库表级和记录级flashback)

应用案例:

  1. 在多个头部游戏,使用单条记录过滤特性,快速查找个别玩家的某段时间的变更流水
  2. 某武侠类游戏,回档频率较高
    以前的回档方式只能回档到每天凌晨冷备,由DBA使用脚本去解析过滤,重新replace。
    使用 mysqlbinlog --flashback --filter-rows ,结合 mysql 的幂等特性,10分钟可回档到任意时间点。(已经平台化)

后面代码合并 tendb3 主仓库,请关注 https://github.com/Tencent/TenDBCluster-TenDB 。

2. 选项说明

相比原生 mysqlbinlog 命令增加了以下选项

-L, --databases=name 
                      List entries for these databases (local log only).Give
                      the database names in a comma separated list.
  --tables=name       List entries for these tables (local log only).Give the
                      tables names in a comma separated list.
  -B, --flashback     Flashback feature can rollback you committed data to a
                      special time point.
  --flashback-databases=name 
                      List entries for these flashback databases (local log
                      only).Give the database names in a comma separated list.
  --flashback-tables=name 
                      List entries for these flashback tables (local log
                      only).Give the tables names in a comma separated list.
  --filter-rows=name  Filter string or file to filter rows from event. (local
                      log only).Format: '@1,@2 100,aaa'. Must work with
                      --tables or --flashback-tables. You can use @2:hex format 
                      to tell mysqlbinlog its' varchar/varbinary/blob value is a hex. 
                      Also you can use @1:signed to mark it as a signed int/
                      tinyint/smallint/mediumint/bigint
  --query-event-handler=name 
                      Decide how to handle the query events like statement or
                      ddl.  Only error|keep|ignore|safe allowed. (error: exit
                      when encountered any query event. keep or ignore all
                      query event. safe: work with --filter-statement-match-error 
                      and --filter-statement-match-ignore )
  --filter-statement-match-error=name 
                      Exit when this string is matched in query event. Comma
                      separated. Only work when
                      query-event-handler=keep|ignore|safe
  --filter-statement-match-ignore=name 
                      Ignore the query event when this string is matched. Comma
                      separated. Only work when query-event-handler=error|safe
  --filter-lines-terminated-by=name 
                      Lines in the filter file are terminated by the given
                      string.
  --filter-fields-terminated-by=name 
                      Fields in the filter file are terminated by the given
                      string.
  --filter-fields-enclosed-by=name 
                      Fields in the filter file are enclosed by the given
                      character.

中文使用说明

  • --tables选项用于 binlog 解析时,只输出指定表的变更记录。多个表以英文 , 分隔,指定多个表时字段位置代表的字段,必须相同
    只针对 delete/insert/update 这样的 row event 有效。
  • --filter-rows选项可以用于根据字段值,过滤 binlog 里面的记录。要求必须指定 --tables(正向) 或者 --flashback-tables(逆向),过滤后的结果可直接应用到 mysql-server 上。

    有两种方式指定:
  • --filter-rows="@2,@3 aaa,10"直接指定值,其中 @2 代表表字段位置,多个字段条件使用 , 分隔。默认通过空格   来区分多行。
    字段位置根据 show create table xxx 顺序往下从 1 开始,或者 information_schema.COLUMNS 表的 ORDINAL_POSITION 里有显示。
    对于 update_rows event,只匹配 before_image 的字段。
  • --filter-rows=keyfilter_back.txt指定csv格式的文件,在需要过滤数量较多的条件时可使用,格式:
@2,@3
aaa,100
bbb,200
  1. 针对 varchar、varbinary, char, binary, blob 等类型,可以指定十六进制格式,例如--filter-rows="@2:hex 0x64646464",对应的字符串为select unhex('64646464') 即 dddd
  2. 针对 tinyint、smallint、mediumint、int、bigint 类型,默认当做无符号 unsigned 处理。当给定的值包括负数时,请指定 @3:signed,否则过滤的结果可能有遗漏。(见下文示例)
  3. 如果过滤条件行较多时,建议将 过滤度 比较高的列字段放在第1列,会提高过滤性能。(不要求是主键)
  4. 请勿将可能修改的字段作为条件,可能导致过滤的结果不对

--filter-lines-terminated-by只在--filter-rows指定时有效,指定行分隔符。指定文件时,默认\n,直接命令行输入时,默认空格。

--filter-fields-terminated-by只在--filter-rows指定时有效,指定字段分隔符。默认,

--filter-fields-enclosed-by只在--filter-rows指定时有效,指定字段包裹分隔符。默认'

--filter-statement-match-errorquery_event 精确匹配了子字符串,退出。多个字符串使用逗号 , 分隔,区分大小写

--filter-statement-match-ignorequery_event 精确匹配了子字符串,忽略,记录到注释。退出。多个字符串使用逗号 , 分隔,区分大小写--filter-statement-match-error 优先匹配生效。

--query-event-handler如何处理 query_event 。像一些 ddl, statement 语句(pt-table-checksum产生的)会记录为 query event,不支持这类语句的过滤和flashback。

允许四个值:error|keep|ignore|safe

  1. error: 遇到 query_event 立马报错退出。但如果同时指定了 --filter-statement-match-ignore,会忽略并记录到注释
  2. keep: 保留 query_event。默认为keep,但如果同时指定了 --filter-statement-match-error,匹配指定字符串后还是会报错退出
  3. ignore: 忽略 query_event,会记录在注释里。如果同时指定了 --filter-statement-match-error,匹配指定字符串后还是会报错退出
  4. safe: 安全模式,配合 --filter-statement-match-error 、 --filter-statement-match-ignore 一起使用
    如果匹配了filter-statement-match-error则退出,否则如果匹配了filter-statement-match-ignore则忽略并记录到注释,都没匹配,退出

3. 核心实现

按记录过滤 binlog row events,实现的核心调用链路 log_event.cc :Log_event::print_base64() => event->filter_rows_from_event() => filter_binlog_one_row() => log_event_filter_value()

  1. 命令行输入的 filiter-rows 过滤选项,query_event 处理选项,处理后存放在 print_event_info 中,它会被传递到sql层处理
  2. filter_rows_from_event() 会对 WRITE_ROWS_EVENT/DELETE_ROWS_EVENT/UPDATE_ROWS_EVENT 事件内容进行过滤,不符合条件的 row 会被裁剪掉,重新拼成新的 row_event (需要更新 event_length)
  3. 因为需要取的每行 row 的列值,原版的log_event_print_value() 只是将 value 以可视化的方法,打印解析结果到注释中。
    现需要改写它成 log_event_filter_value(),将各种字段类型(tinyint/small/int/bigint/float/double/decimal/char/varchar/text/binary/varbinary/blob/json等等),全都以 string 的格式返回,用于后续比较。
  4. 针对 filter-rows 可能有上千行的情况,为了加快比较速度,使用一个 map 存放第一个字段的值,通过 map key的查找代替每次循环比较所有行。

log_event.cc 中 filter_rows_from_event() 伪代码:

if (enable_filter_rows) {
	  uint tmp_size = size;
	  Rows_log_event *ev = NULL;
	  Log_event_type ev_type = (enum Log_event_type) ptr[EVENT_TYPE_OFFSET];
	  enum_binlog_checksum_alg  checksum_alg = (ev_type != binary_log::FORMAT_DESCRIPTION_EVENT) ? common_footer->checksum_alg :
		  Log_event_footer::get_checksum_alg(temp_buf, (unsigned long)size);
	  if (checksum_alg != binary_log::BINLOG_CHECKSUM_ALG_UNDEF &&
		  checksum_alg != binary_log::BINLOG_CHECKSUM_ALG_OFF)
		  tmp_size -= BINLOG_CHECKSUM_LEN; // checksum is displayed through the header
	  switch (ev_type) {
	  case binary_log::WRITE_ROWS_EVENT:
		  ev = new Write_rows_log_event((const char*)ptr, tmp_size,
			  glob_description_event);
		  filter_result = ev->filter_rows_from_event(print_event_info, ptr, ev_type);
		  // change the row event body size
		  if (tmp_size < size) {
			  int4store(ptr + filter_result.first, my_checksum(0L, (uchar *)ptr, filter_result.first));
			  size = filter_result.first + BINLOG_CHECKSUM_LEN;
		  }
		  else {
			  size = filter_result.first;
		  }
		  int4store(ptr + EVENT_LEN_OFFSET, size);
		  break;
...
	  case binary_log::DELETE_ROWS_EVENT:
		  ev = new Delete_rows_log_event((const char*)ptr, tmp_size,
			  glob_description_event);
		  filter_result = ev->filter_rows_from_event(print_event_info, ptr, ev_type);
		  // change the row event body size
		  size = (tmp_size < size) ? (filter_result.first + BINLOG_CHECKSUM_LEN) : filter_result.first;
		  int4store(ptr + EVENT_LEN_OFFSET, size);
		  break;
...
	  case binary_log::UPDATE_ROWS_EVENT:
	  case binary_log::UPDATE_ROWS_EVENT_V1:
		  ev = new Update_rows_log_event((const char*)ptr, tmp_size,
			  glob_description_event);
		  filter_result = ev->filter_rows_from_event(print_event_info, ptr, ev_type);
		  // change the row event body size
		  size = (tmp_size < size) ? (filter_result.first + BINLOG_CHECKSUM_LEN) : filter_result.first;
		  int4store(ptr + EVENT_LEN_OFFSET, size);
		  break;
	  default:
		  break;
	  }
...
		  delete ev;
  }

4. 使用示例

准备一个表结构(包含int signed/unsigned,varchar,datetime,varbinary,blob),初始化数据:

mysql> show create table filter_row_test\G
*************************** 1. row ***************************
       Table: filter_row_test
Create Table: CREATE TABLE `filter_row_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(60) NOT NULL,
  `nickname` varbinary(60) DEFAULT NULL,
  `age` smallint(5) unsigned NOT NULL DEFAULT '0',
  `gender` char(5) DEFAULT NULL,
  `hight` float DEFAULT '0',
  `weight` decimal(8,3) DEFAULT '0.000',
  `birthday` datetime DEFAULT NULL,
  `create_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `image` blob,
  `newid` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2147483647 DEFAULT CHARSET=utf8
mysql> select id,username,nickname,gender,hight,weight,birthday,create_ts,newid from filter_row_test;
+------+----------+----------+--------+--------+---------+---------------------+---------------------+------------+
| id   | username | nickname | gender | hight  | weight  | birthday            | create_ts           | newid      |
+------+----------+----------+--------+--------+---------+---------------------+---------------------+------------+
|   -1 | hhh      | NULL     | NULL   |      0 |   0.000 | NULL                | 2020-12-25 11:01:10 | 2147483648 |
|  100 | aaa      | aaaa     | M      |  100.1 | 100.100 | 2020-12-02 00:00:01 | 2020-12-02 00:00:01 |       NULL |
|  200 | aaa      | aaaa     | M      |  200.1 | 200.100 | 2020-12-02 00:00:02 | 2020-12-02 00:00:02 |       NULL |
|  300 | ccc      | cccc     | F      | 300.01 | 200.100 | 2020-12-02 00:00:03 | 2020-12-02 00:00:03 |       NULL |
|  400 | d中文d   | dddd     | F      |  200.1 | 200.100 | 2020-12-02 00:00:04 | 2020-12-02 00:00:04 |       NULL |
|  500 | ddd      | 㾄        | F      |  200.1 | 200.100 | 2020-12-02 00:00:05 | 2020-12-02 00:00:05 |       NULL |
|  600 | ddd      | 㾄        | F      |  200.1 | 200.100 | 2020-12-02 00:00:06 | 2020-12-02 00:00:06 |       NULL |
| 1000 | ee e     | ee ee    | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |       NULL |
| 1001 | ee "e    | ee "ee   | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |       NULL |
| 1002 | ee ,e    | ee ,ee   | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |       NULL |
+------+----------+----------+--------+--------+---------+---------------------+---------------------+------------+
10 rows in set (0.00 sec)

-- 做一些 dml 操作
flush logs;
delete from filter_row_test where id=100;
update filter_row_test set age=66 where username='ddd';
delete from filter_row_test where username in ('ee e','ee "e');
insert into filter_row_test(id,username, age,hight,create_ts) values(2000, 'fffff', 10, 100.10, '2020-12-03 13:01:01'),(2001, 'fff111', 10, 100.10, '2020-12-03 13:01:02');
update filter_row_test set age=76,hight=101.1 where username='ddd';
delete from filter_row_test; -- delete全表
flush logs;

4.1 过滤示例(正向):

#  按表来过滤binlog,可以不指定 --databases
$ mysqlbinlog binlog20000.000025 -vv --tables filter_row_test  > filter_row_test.sql

$ mysqlbinlog binlog20000.000025 -vv --tables filter_row_test --filter-rows="@1,@2 200,aaa" > filter_row_test.aaa.sql

上面两个过滤命令,我们对比 delete 全表那条命令:

全表过滤 filter_row_test.sql:(注意下这里@1=-1 的输出)

# at 314
#201225 15:08:49 server id 81482679  end_log_pos 401 CRC32 0x9d20450b   Table_map: `xxx`.`filter_row_test` mapped to number 494
# at 401
#201225 15:08:49 server id 81482679  end_log_pos 1310 CRC32 0x240f60f4  Delete_rows: table id 494 flags: STMT_END_F

BINLOG '
AZDlXxO3U9sEVwAAAJEBAAAAAO4BAAAAAAEACXhpYW9nemhvdQAPZmlsdGVyX3Jvd190ZXN0AAwD
Dw8C/gT2EhH8AwMMtAA8AP4PBAgDAAAC9A4LRSCd
AZDlXyC3U9sEjQMAAB4FAAAAAO4BAAAAAAEAAgAM//+U+v8DaGhoAAAAAAAAgAAAAABf5VbP
AAAAgAD+yAAAAANhYWEEYWFhYRQAAU2aGUhDgADIAGSZqAQAAl/GaIIA/iwBAAADY2NjBGNjY2MU
AAFGSAGWQ4AAyABkmagEAANfxmiDAP6QAQAACGTkuK3mlodkBGRkZGQoAAFGmhlIQ4AAyABkmagE
AARfxmiEAPz0AQAAA2RkZAPkgARMAAFGMzPKQoAAyABkmagEAAVf5Y8gLgKD9oIqugOEAIUshAEI
...
AAVmZmZmZgoAMzPIQoAAAAAAX8hxDZT+0QcAAAZmZmYxMTEKADMzyEKAAAAAAF/IcQ70YA8k
'/*!*/;
### DELETE FROM `xxx`.`filter_row_test`
### WHERE
###   @1=-1 (4294967295)
###   @2='hhh'
###   @3=NULL
###   @4=0
###   @5=NULL
###   @6=0                   
###   @7=0.000
###   @8=NULL
###   @9=1608865487
###   @10=NULL
###   @11=-2147483648 (2147483648)
###   @12=NULL
### DELETE FROM `xxx`.`filter_row_test`
### WHERE
###   @1=200
###   @2='aaa'
###   @3='aaaa'
###   @4=20
...

指定行过滤 filter_row_test.aaa.sql:

# at 314
#201225 15:08:49 server id 81482679  end_log_pos 401 CRC32 0x9d20450b   Table_map: `xxx`.`filter_row_test` mapped to number 494
# at 401
#201225 15:08:49 server id 81482679  end_log_pos 1310 CRC32 0x240f60f4  Delete_rows: table id 494 flags: STMT_END_F

BINLOG '
AZDlXxO3U9sEVwAAAJEBAAAAAO4BAAAAAAEACXhpYW9nemhvdQAPZmlsdGVyX3Jvd190ZXN0AAwD
Dw8C/gT2EhH8AwMMtAA8AP4PBAgDAAAC9A4LRSCd
AZDlXyC3U9sESQAAAB4FAAAAAO4BAAAAAAEAAgAM//8A/sgAAAADYWFhBGFhYWEUAAFNmhlIQ4AA
yABkmagEAAJfxmiCYWEEYQ==
'/*!*/;
### DELETE FROM `xxx`.`filter_row_test`
### WHERE
###   @1=200
###   @2='aaa'
###   @3='aaaa'
###   @4=20
###   @5='M'
###   @6=200.1               
###   @7=200.100
###   @8='2020-12-02 00:00:02'
###   @9=1606838402
###   @10=NULL
###   @11=NULL
###   @12=NULL

看到内容被裁剪了,只保留了符合条件 id=200 and username='aaa' 的row。

指定字段条件过滤 binlog,多行:

$ mysqlbinlog binlog20000.000025 -vv --tables filter_row_test  --filter-rows="@1 100 500" > 100.sql

# 也可以指定csv文件作为过滤条件
$ cat key.txt
@2
'ee e'
aaa
ddd

$ mysqlbinlog binlog20000.000025 -vv --tables filter_row_test  --filter-rows=keyback.txt > 100.sql

# 过滤值为为负数的列
$ mysqlbinlog -v binlog20000.000044 --flashback --flashback-tables filter_row_test --filter-rows="@1 -1"
这个无法过滤出 id=-1 的数据,必须指定 "@1:signed -1",或者 "@1 4294967295"

结合 --query-event-handler 过滤危险 query event:

-- alter table roll_test add column c2 int;
-- alter table filter_row_test add column c2 int;
-- alter table roll_test drop column c2;

-- alter table filter_row_test drop column c2;


$ mysqlbinlog binlog20000.000041 -v binlog20000.000041 --query_event_handler=ignore > binlog20000.000041.sql
会记录到注释:
# ignore query_log_event
# alter table filter_row_test add column c2 int


下面是一个典型组合

$ mysqlbinlog binlog20000.000041 -v binlog20000.000041 \
--tables=filter_row_test \
--filter-rows=xxx.txt \
--query-event-handler=safe \
--filter-statement-match-ignore="FLUSH ,db_infobase,create table ,CREATE TABLE " \
--filter-statement-match-error="filter_row_test" \
--start-datetime='2020-12-18 02:08:01'\
--stop-datetime='2020-12-22 16:13:42' \
> binlog20000.000041.sql
Exit when query event occurs: alter table filter_row_test add column c2 int

效果:想要回滚 filter_row_test 表的数据,但回滚期间该表不能有ddl,其它表上有 ddl 或者 query event 不影响回滚。

这里演示的会解析失败退出,因为 filter_row_test 上有 ddl ,符合预期。

4.2 结合--flashback(反向)回滚示例:

上面的普通正向过滤语句,都可以加 --flashback --flashback-tables xxx 转换成回滚语句。
回滚 id in (2000,2001) 两条insert:

$ mysqlbinlog binlog20000.000025 -vv --flashback --flashback-tables filter_row_test --filter-rows='@1 2000 2001' > 2000.back.sql
$ cat 2000.back.sql | mysql

mysql> select id,username,nickname,age,gender,hight,weight,birthday,create_ts from filter_row_test;
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
| id   | username | nickname | age | gender | hight  | weight  | birthday            | create_ts           |
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
...
|  500 | ddd      | 㾄        |  76 | F      |  101.1 | 200.100 | 2020-12-02 00:00:05 | 2020-12-03 15:43:08 |
|  600 | ddd      | 㾄        |  76 | F      |  101.1 | 200.100 | 2020-12-02 00:00:06 | 2020-12-03 15:43:08 |
| 1002 | ee ,e    | ee ,ee   |  20 | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
6 rows in set (0.00 sec)

回滚 username in ('ee e','ee "e') 两条delete,考虑到改字段有特殊字符,转成十六进制:

$ mysqlbinlog binlog20000.000025 -vv --flashback --flashback-tables filter_row_test \
 --filter-rows='@2:hex 0x65652065 0x6565202265' > eee.back.sql

# 注意:字段里有空格,命令行默认以空格分行,如果不转成十六进制,需要指定分行符(与上等价)
$ mysqlbinlog binlog20000.000025 -vv --flashback --flashback-tables filter_row_test  --filter-rows='@2\nee e\nee "e' --filter-lines-terminated-by='\n'

$ cat eee.back.sql | mysql

mysql> select id,username,nickname,age,gender,hight,weight,birthday,create_ts from filter_row_test;
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
| id   | username | nickname | age | gender | hight  | weight  | birthday            | create_ts           |
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
...
| 1000 | ee e     | ee ee    |  20 | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |
| 1001 | ee "e    | ee "ee   |  20 | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |
| 1002 | ee ,e    | ee ,ee   |  20 | F      | 300.01 | 200.100 | 2020-12-02 00:00:07 | 2020-12-02 00:00:07 |
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+

把 username=ddd 的 update 也回滚下,这次使用文件输入:

$ cat ddd_1.txt
@1,@2
500,ddd

$ mysqlbinlog binlog20000.000025 -vv --flashback --flashback-tables filter_row_test  --filter-rows=ddd_1.txt  > ddd1.back.sql

cat ddd1.back.sql | mysql

mysql> select id,username,nickname,age,gender,hight,weight,birthday,create_ts from filter_row_test;
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
| id   | username | nickname | age | gender | hight  | weight  | birthday            | create_ts           |
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
...
|  500 | ddd      | 㾄        |  50 | F      |  200.1 | 200.100 | 2020-12-02 00:00:05 | 2020-12-02 00:00:05 |
|  600 | ddd      | 㾄        |  76 | F      |  101.1 | 200.100 | 2020-12-02 00:00:06 | 2020-12-03 15:43:08 |
...
+------+----------+----------+-----+--------+--------+---------+---------------------+---------------------+
# 这条记录有被update两次,结果符合预期

第3个字段是varbinary,使用十六进制:

$ cat ddd_2.txt
@3:hex
0xE48004
0x61616161

$ mysqlbinlog binlog20000.000025 -vv --flashback --flashback-tables filter_row_test  --filter-rows=ddd_2.txt  > ddd2.back.sql

通过以上各种条件回滚,表的数据与之前一样了。

后续方向:

  1. 提供选项,将 UPDATE_ROWS_EVENT 转换成 WRITE_ROWS_EVENT,用于在幂等模式下 update 空行忽略执行的情况
  2. 提供选项,同时过滤多个表的不同字段