MySQL replace command, not recommended.

MySQL replace operation causes inconsistency between primary and secondary self incrementing primary keys

A problem encountered online today is the inconsistency of master-slave self increasing primary keys caused by replace syntax. Here I simulate it and the problem can be reproduced stably. I hope you don't step on the pit in the follow-up process

01

Problem restore

Environment introduction:

MySQL version 5.7.18

Introduction to key parameters:

binlog_format: row

binlog_row_image:full

Main library operation

Create a table test on the main database and insert some data:

# Create table
create table test1 (
id int not null auto_increment primary key, # Primary key
name varchar(10) unique,  # unique index
age int  );
Query OK, 0 rows affected (0.13 sec)
# insert data
insert into test1 values (1,'aaa',1),(2,'bbb',2);
Query OK, 2 rows affected (0.04 sec)
Records: 2  Duplicates: 0  Warnings: 0

# Insert data with replace
replace into test1 values (3,'ccc',3);
Query OK, 1 row affected (0.04 sec)

# View data
select * from test1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | aaa  |    1 |
|  2 | bbb  |    2 |
|  3 | ccc  |    3 |
+----+------+------+
3 rows in set (0.03 sec)
copy

At this time, it can be seen that when replacing inserts data with id=3, it can be inserted normally. The return is 1 row affected, which means that 1 row of data has been affected.

Then we use the replace syntax to insert the column with id=4 again:

replace into test1 values (4,'aaa',4);
Query OK, 2 rows affected (0.03 sec)


select * from test1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | bbb  |    2 |
|  3 | ccc  |    3 |
|  4 | aaa  |    4 |
+----+------+------+
3 rows in set (0.04 sec)
copy

Note that the return value here is 2 lines. Why 2 lines? This is explained by the words in the official documents:

REPLACE works exactly like INSERT, except that if an old row in the table has the same value as a new row for a PRIMARY KEY or a UNIQUE index, the old row is deleted before the new row is inserted.

From the above description, it is not difficult to see that when replacing encounters primary key conflict or unique key conflict, it executes delete first and then insert.

Therefore, when we look at the contents of the table, we can see that since the name column with id=4 is' aaa ', there is a unique key conflict with the name column with id=1, the record with id=1 does not exist, and the record with id=4 is replaced.

At this point, let's view the table structure of the main library:

show create table test1\G
*************************** 1. row ***************************
       Table: test1
Create Table: CREATE TABLE `test1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4
1 row in set (0.14 sec)
copy

We can see that since the last operation was deleted and then inserted, the auto of the main library_ Increment has become 5.

Operation from library

Let's take another look at the data records and self increasing id values from the database;

select * from test1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | bbb  |    2 |
|  3 | ccc  |    3 |
|  4 | aaa  |    4 |
+----+------+------+
3 rows in set (0.13 sec)

show create table test1\G
*************************** 1. row ***************************
       Table: test1
Create Table: CREATE TABLE `test1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
1 row in set (0.13 sec)
copy

As you can see, auto from the library_ The increment value is 4.

You can see two questions:

1. Auto from library_ Increment value and main library Auto_ Inconsistent increment value.

2,AUTO_ The value of increment represents the default ID of the next record inserted into the table, but there is already a record with id=4 in our slave database

02

Cause analysis

In fact, the essential reason for this problem is that MySQL combines the delete and insert operations of the replace statement into an update statement in binlog, and the update statement only updates the value of the id column in the record without actively updating auto_ The value of increment (please understand this sentence). When we parse the corresponding binlog log, we can see:

replace id=3 Recorded, recorded binlog yes insert operation

BEGIN
/*!*/;
# at 139995629
#220413 20:22:45 server id 2157944972  end_log_pos 139995681 CRC32 0xea7d7120   Table_map: `test`.`test1` mapped to number 153
# at 139995681
#220413 20:22:45 server id 2157944972  end_log_pos 139995729 CRC32 0x5b8b0ebc   Write_rows: table id 153 flags: STMT_END_F
### INSERT INTO `test`.`test1`
### SET
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2='ccc' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @3=3 /* INT meta=0 nullable=1 is_null=0 */
# at 139995729
#220413 20:22:45 server id 2157944972  end_log_pos 139995760 CRC32 0xc69ecc26   Xid = 12340849656
COMMIT/*!*/;


replace id=4 Recorded, recorded binlog yes update operation

BEGIN
/*!*/;
# at 139996477
#220413 20:22:58 server id 2157944972  end_log_pos 139996529 CRC32 0xb8805056   Table_map: `test`.`test1` mapped to number 153
# at 139996529
#220413 20:22:58 server id 2157944972  end_log_pos 139996591 CRC32 0x07d4f31b   Update_rows: table id 153 flags: STMT_END_F
### UPDATE `test`.`test1`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2='aaa' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @3=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=4 /* INT meta=0 nullable=0 is_null=0 */
###   @2='aaa' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @3=4 /* INT meta=0 nullable=1 is_null=0 */
# at 139996591
#220413 20:22:58 server id 2157944972  end_log_pos 139996622 CRC32 0xe0cf7229   Xid = 12340849733
COMMIT/*!*/;
copy

In the process of this experiment, I tested mysql8 Version 0 and MySQL 5.0 Version 7, found mysql8 0. Although the binlog content is consistent, auto is updated_ Value of increment.

This phenomenon can be understood as a bug in MySQL version 5.7.

03

potential impact

You may think that if the master database inserts a new data record without conflict by using the replace operation, the self increment of the slave database will be synchronized again. It doesn't seem to have any impact on the whole architecture???

This idea is wrong. Imagine such a scenario. If the master-slave switch occurs at this time point, the slave library will be promoted to a new master library. Take our example above:

At this time, the data records in the new main database are as follows:

select * from test1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | bbb  |    2 |
|  3 | ccc  |    3 |
|  4 | aaa  |    4 |
+----+------+------+
3 rows in set (0.13 sec)
copy

But the new main library's auto_ The increment value is 4, which means that the next insert statement in the new main database that does not specify the self increment ID will report a primary key conflict. Because it will generate a record with self incrementing id=4 by default, but this record already exists in the table.

04

summary

Since the above Auto_ Inconsistent increment values will bring hidden dangers after master-slave switching. How should we eliminate them?

1. Upgrade MySQL version to version 8.0.

2. The business side shall eliminate the non-standard SQL syntax of replace and use the business logic to judge the data conflict.

3. Detect the inconsistency of self increment ID, configure corresponding monitoring, find and solve the problem at the first time.

Posted by mo0ness on Tue, 17 May 2022 08:01:19 +0300