The most perverted MySQL DML flashback tool
- MySQL Flashback
In the era when MySQL Binlog has been played to death, everyone should know the principle by now. It's nothing more than parsing the Binlog to generate rollback SQL statements in reverse.
Frankly speaking, there are many implementations of such tools online, but none are satisfactory.
Most online implementations are developed in Python. However, when dealing with large volumes, there are often various problems. So I decided to reimplement one in Golang.
DBAs who know a bit of Python should be familiar with the
python-mysql-replicationpackage for parsing MySQL Binlog. Thepython-mysql-replicationpackage has a timestamp bug, I don't know if it's been fixed. I had fixed it in my project before, but didn't submit a PR.
go build
-
Specify start and end rollback positions
-
Specify start and end rollback times
-
Start time and position can be mixed, if both are specified, position takes precedence
-
Specify multiple databases and tables
-
Specify thread id
-
Can use specified SQL statements to get the parameters needed for rollback above
-
Can support conditional filtering, conditional filtering is also expressed in SQL form
./mysql-flashback create \
--match-sql="SELECT col_1, col_2, col_3 FROM schema.table WHERE col_1 = 1 AND col_2 IN(1, 2, 3) AND col_3 BETWEEN 10 AND 20 AND start_log_file = 'mysql-bin.000001' AND start_log_pos = 4 AND end_log_file = 'mysql-bin.000004' AND end_log_pos = 0 AND start_rollback_time = '2019-06-06 12:00:01' AND end_rollback_time = '2019-06-07 12:00:01' AND thread_id = 0"
The --match-sql parameter value is quite long, mainly because the parameters have to be in one string.
Formatted look at the --match-sql parameter value
SELECT col_1, col_2, col_3 -- Specify only the fields needed, SELECT * means all fields
FROM schema.table -- Table to rollback. Must explicitly specify the database the table belongs to
WHERE col_1 = 1 -- Filter condition
AND col_2 IN(1, 2, 3) -- Filter condition IN expression
AND col_3 BETWEEN 10 AND 20 -- Filter condition BETWEEN ... AND ... expression
AND start_log_file = 'mysql-bin.000001' -- Specify rollback range (start binlog [file]), not a filter condition.
AND start_log_pos = 4 -- Specify rollback range (start binlog [position]), not a filter condition.
AND end_log_file = 'mysql-bin.000004' -- Specify rollback range (end binlog [file]), not a filter condition.
AND end_log_pos = 0 -- Specify rollback range (end binlog [position]), not a filter condition.
AND start_rollback_time = '2019-06-06 12:00:01' -- Specify rollback range (start time), not a filter condition.
AND end_rollback_time = '2019-06-07 12:00:01' -- Specify rollback range (end time), not a filter condition.
AND thread_id = 0 -- Specify Thread ID to rollback.
After formatting the SQL to look nice, it's clear what the SQL is doing:
- The following conditions specify the parameters for parsing Binlog.
AND start_log_file = 'mysql-bin.000001' -- Specify rollback range (start binlog [file]), not a filter condition.
AND start_log_pos = 4 -- Specify rollback range (start binlog [position]), not a filter condition.
AND end_log_file = 'mysql-bin.000004' -- Specify rollback range (end binlog [file]), not a filter condition.
AND end_log_pos = 0 -- Specify rollback range (end binlog [position]), not a filter condition.
AND start_rollback_time = '2019-06-06 12:00:01' -- Specify rollback range (start time), not a filter condition.
AND end_rollback_time = '2019-06-07 12:00:01' -- Specify rollback range (end time), not a filter condition.
AND thread_id = 0 -- Specify Thread ID to rollback.
- The following conditions specify filtering on the data. When generating rollback statements, it will match the conditions you specify.
WHERE col_1 = 1 -- Filter condition
AND col_2 IN(1, 2, 3) -- Filter condition IN expression
AND col_3 BETWEEN 10 AND 20 -- Filter condition BETWEEN ... AND ... expression
- The following specifies the table to rollback
FROM schema.table -- Table to rollback. Must explicitly specify the database the table belongs to
- The following specifies which fields the generated rollback statement should include, mainly for UPDATE, for INSERT it must be all field values.
SELECT col_1, col_2, col_3 -- Specify only the fields needed, SELECT * means all fields
./mysql-flashback create \
--match-sql="Specified SQL1" \
--match-sql="Specified SQL2"
Traditional way of specifying parameter values, you can execute ./mysql-flashback create --help to see usage examples
./mysql-flashback create --help
Generate rollback sql. As follows:
Example:
Specify start position and end position
./mysql-flashback create \
--start-log-file="mysql-bin.000090" \
--start-log-pos=0 \
--end-log-file="mysql-bin.000092" \
--end-log-pos=424 \
--thread-id=15 \
--rollback-table="schema1.table1" \
--rollback-table="schema1.table2" \
--rollback-table="schema2.table1" \
--save-dir="" \
--db-host="127.0.0.1" \
--db-port=3306 \
--db-username="root" \
--db-password="root" \
--match-sql="select * from schema1.table1 where name = 'aa'"
Specify start position and end time
./mysql-flashback create \
--start-log-file="mysql-bin.000090" \
--start-log-pos=0 \
--end-time="2018-12-17 15:36:58" \
--thread-id=15 \
--rollback-table="schema1.table1" \
--rollback-table="schema1.table2" \
--rollback-table="schema2.table1" \
--save-dir="" \
--db-host="127.0.0.1" \
--db-port=3306 \
--db-username="root" \
--db-password="root" \
--match-sql="select name, age from schema1.table1 where name = 'aa'"
Specify start time and end time
./mysql-flashback create \
--start-time="2018-12-14 15:00:00" \
--end-time="2018-12-17 15:36:58" \
--thread-id=15 \
--rollback-schema="schema1" \
--rollback-table="table1" \
--rollback-table="schema1.table2" \
--rollback-table="schema2.table1" \
--save-dir="" \
--db-host="127.0.0.1" \
--db-port=3306 \
--db-username="root" \
--db-password="root" \
--match-sql="select name, age from schema1.table1 where name = 'aa' and age = 2"
Usage:
mysql-flashback create [flags]
Flags:
--db-auto-commit Database auto commit (default true)
--db-charset string Database charset (default "utf8mb4")
--db-host string Database host (default "127.0.0.1")
--db-max-idel-conns int Database max idle connections (default 8)
--db-max-open-conns int Database max open connections (default 8)
--db-password string Database password (default "root")
--db-password-is-decrypt Whether database password needs decryption (default true)
--db-port int Database port (default 3306)
--db-schema string Database name
--db-timeout int Database timeout (default 10)
--db-username string Database username (default "root")
--enable-rollback-delete Whether to enable rollback delete (default true)
--enable-rollback-insert Whether to enable rollback insert (default true)
--enable-rollback-update Whether to enable rollback update (default true)
--end-log-file string End log file
--end-log-pos uint32 End log file position
--end-time string End time
-h, --help help for create
--match-sql string Use simple SELECT statement to match needed fields and records
--rollback-schema strings Specify databases to rollback, this command can specify multiple
--rollback-table strings Tables to rollback, this command can specify multiple
--save-dir string Path to save related files
--start-log-file string Start log file
--start-log-pos uint32 Start log file position
--start-time string Start time
--thread-id uint32 Thread id to rollback
After executing ./mysql-flashback create ..., two SQL files will be generated
-
The original SQL statement file, the statements in the file are not the statements executed at that time, but statements representing the amount of data affected. For example: An UPDATE statement that affects 10 rows, then the original SQL statement will also be 10 statements.
-
The rollback SQL file
-
Since the rollback statements are written to the file in sequence, when we rollback, we need to read the SQL file in reverse order for rollback.
-
When looking at the rollback statements, you will find that each SQL has a comment
/* crc32:xxx */in front, the valuexxxhere records a crc32 value of each data's primary key. This is mainly recorded for concurrency.
/* crc32:2313941001 */ INSERT INTO `employees`.`emp1`(`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES(10008, "1958-02-19", "Saniya", "Kalloufi", 1, "1994-09-15");
Use ./mysql-flashback execute --help to see that there is a usage example
./mysql-flashback execute --help
Execute the specified sql rollback file in reverse order. As follows:
Example:
./mysql-flashback execute \
--filepath="/tmp/test.sql" \
--paraller=8 \
--db-host="127.0.0.1" \
--db-port=3306 \
--db-username="root" \
--db-password="root"
Usage:
mysql-flashback execute [flags]
Flags:
--db-auto-commit Database auto commit (default true)
--db-charset string Database charset (default "utf8mb4")
--db-host string Database host
--db-max-idel-conns int Database max idle connections (default 8)
--db-max-open-conns int Database max open connections (default 8)
--db-password string Database password
--db-password-is-decrypt Whether database password needs decryption (default true)
--db-port int Database port (default -1)
--db-schema string Database name
--db-timeout int Database timeout (default 10)
--db-username string Database username
--filepath string Specified file to execute
-h, --help help for execute
--paraller int Number of rollback parallels (default 1)
The rollback information generation shown earlier uses simulating a slave connection to MySQL to get binlog events, but often the MySQL connection no longer exists, so you need to specify offline binlog for generating rollback information
Parse offline binlog, generate rollback SQL. As follows:
Example:
./mysql-flashback offline \
--enable-rollback-insert=true \
--enable-rollback-update=true \
--enable-rollback-delete=true \
--thread-id=15 \
--save-dir="" \
--schema-file="" \
--match-sql="select * from schema1.table1 where name = 'aa'" \
--match-sql="select * from schema2.table1 where name = 'aa'" \
--binlog-file="mysql-bin.0000001" \
--binlog-file="mysql-bin.0000002"
Usage:
mysql-flashback offline [flags]
Flags:
--binlog-file stringArray Which binlog files
--enable-rollback-delete Whether to enable rollback delete (default true)
--enable-rollback-insert Whether to enable rollback insert (default true)
--enable-rollback-update Whether to enable rollback update (default true)
-h, --help help for offline
--match-sql stringArray Use simple SELECT statement to match needed fields and records
--save-dir string Path to save related files
--schema-file string Table structure file
--thread-id uint32 Thread id to rollback
Generate 4 types of statistics, saved in related directory files:
-
offline_stat_output/table_stat.txt: Table-related statistics (sorted by affected rows)
-
offline_stat_output/thread_stat.txt: ThreadId-related statistics (sorted by affected rows)
-
offline_stat_output/timestamp_stat.txt: Time-related statistics, statistics by seconds, the time recorded is when the transaction executes BEGIN, one time point may have multiple transactions.
-
offline_stat_output/xid_stat.txt: Transaction-related statistics, statistics for each transaction, sorted by xid representing different transactions
./mysql-flashback offline-stat --help
Parse offline binlog, generate binlog statistics. As follows:
After successful execution, 4 files will be generated in the current directory
offline_stat_output/table_stat.txt # Save table statistics
offline_stat_output/thread_stat.txt # Save thread statistics
offline_stat_output/timestamp_stat.txt # Save time statistics (records the time when each transaction executes BEGIN)
offline_stat_output/xid_stat.txt # Save xid statistics
Example:
./mysql-flashback offline-stat \
--save-dir="offline_stat_output" \
--binlog-file="mysql-bin.0000001" \
--binlog-file="mysql-bin.0000002"
Usage:
mysql-flashback offline-stat [flags]
Flags:
--binlog-file stringArray Which binlog files
-h, --help help for offline-stat
--save-dir string Statistics save directory (default "offline_stat_output")
Table: employees.emp_01 dml affected rows: 100, insert: 100, update: 0, delete: 0, table occurrences: 1
Table: employees.emp dml affected rows: 10, insert: 0, update: 7, delete: 3, table occurrences: 3
threadId: 464 dml affected rows: 110, insert: 100, update: 7, delete: 3, table occurrences: 4
2023-08-11 14:35:06: dml affected rows: 1, insert: 0, update: 0, delete: 1, transactions: 1, start position: /Users/hh/Desktop/mysql-bin.000200:395
2023-08-11 14:35:20: dml affected rows: 2, insert: 0, update: 0, delete: 2, transactions: 1, start position: /Users/hh/Desktop/mysql-bin.000200:749
2023-08-11 14:36:22: dml affected rows: 100, insert: 100, update: 0, delete: 0, transactions: 1, start position: /Users/hh/Desktop/mysql-bin.000200:1147
2023-08-11 14:37:28: dml affected rows: 7, insert: 0, update: 7, delete: 0, transactions: 1, start position: /Users/hh/Desktop/mysql-bin.000200:4217
Xid: 7500 2023-08-11 14:35:06 dml affected rows: 1, insert: 0, update: 0, delete: 1, start position: /Users/hh/Desktop/mysql-bin.000200:395
Xid: 7501 2023-08-11 14:35:20 dml affected rows: 2, insert: 0, update: 0, delete: 2, start position: /Users/hh/Desktop/mysql-bin.000200:749
Xid: 7504 2023-08-11 14:36:22 dml affected rows: 100, insert: 100, update: 0, delete: 0, start position: /Users/hh/Desktop/mysql-bin.000200:1147
Xid: 7507 2023-08-11 14:37:28 dml affected rows: 7, insert: 0, update: 7, delete: 0, start position: /Users/hh/Desktop/mysql-bin.000200:4217
This project implements several security best practices to minimize vulnerabilities:
- Distroless Base Image: Uses
gcr.io/distroless/static-debian12:nonrootfor minimal attack surface - Non-root User: Runs as
nonroot:nonrootuser with minimal privileges - No Package Managers: Final image contains no package managers or shells
- Static Binary: Compiled with static linking and stripped symbols
- Read-only Filesystem: Container runs with read-only root filesystem
- Security Updates: Alpine base image is updated with latest security patches
- Dependency Scanning: Regular vulnerability checks on Go dependencies
- Secure Build Flags: Uses
-w -sflags to strip debugging information - Multi-stage Build: Separates build and runtime environments
Run security scans using the provided tools:
# Run comprehensive security scan
./security-scan.sh
# Build and scan Docker image
make docker-build-scan
# Scan source code for vulnerabilities
make security-scanInstall recommended security scanning tools:
# Vulnerability scanner
brew install trivy
# Docker image scanner
brew install goodwithtech/r/dockle
# Go security linter
go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
# Go vulnerability checker
go install golang.org/x/vuln/cmd/govulncheck@latest
# Secret scanner
brew install gitleaks- Regular Updates: Keep dependencies and base images updated
- Secret Management: Never hardcode credentials in source code
- Network Security: Use TLS/SSL for database connections
- Access Control: Limit database user privileges to minimum required
- Audit Logging: Enable comprehensive logging for security monitoring
- Backup Security: Encrypt and secure backup files
- Testing: Test rollback procedures in staging before production use
When running the container, use additional security options:
docker run --rm \
--user nonroot:nonroot \
--read-only \
--tmpfs /tmp \
--security-opt no-new-privileges \
--cap-drop ALL \
mysql-flashback:latest --helpIf you discover a security vulnerability, please report it responsibly by:
- Creating a GitHub issue with "SECURITY" in the title
- Providing detailed information about the vulnerability
- Allowing time for the issue to be addressed before public disclosure