概念描述

mysql5.7版本在使用replace into语法往表中插入记录时,如果违反主键或唯一键约束,则会进行数据"覆盖",但这种"覆盖"的规则,会引起一定的问题。下面从2方面来说明问题:

  • replace into数据"覆盖"规则
  • replace into主从切换后主键冲突

测试验证

replace into数据"覆盖"规则

假如一张表上同时存在主键和唯一键,我们分5种情况进行分析:

  • 主键不冲突 + 唯一键不冲突
  • 主键不冲突 + 唯一键冲突
  • 主键冲突 + 唯一键不冲突
  • 主键冲突 + 唯一键冲突(同一行)
  • 主键冲突 + 唯一键冲突(两行)

创建测试数据:

mysql> use modb;
Database changed
mysql> drop table if exists t_replaceinto;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> CREATE TABLE t_replaceinto (
    ->   `id` int NOT NULL AUTO_INCREMENT,
    ->   `c` int DEFAULT NULL,
    ->   PRIMARY KEY (`id`),
    ->   UNIQUE KEY `c` (`c`)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.03 sec)

情况1:主键不冲突 + 唯一键不冲突

#插入2条不存在的记录
mysql> replace into t_replaceinto(c) values(1),(2);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

#查看相关binlog
### INSERT INTO `modb`.`t_replaceinto`
### SET
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### INSERT INTO `modb`.`t_replaceinto`
### SET
###   @1=2 /* INT meta=0 nullable=0 is_null=0 */
###   @2=2 /* INT meta=0 nullable=1 is_null=0 */

从结果得出:相当于insert操作。

情况2:主键不冲突 + 唯一键冲突

#插入c=2的记录,让唯一键冲突
mysql> replace into t_replaceinto(c) values(2);
Query OK, 2 rows affected (0.02 sec)

#查看相关binlog
### UPDATE `modb`.`t_replaceinto`
### WHERE
###   @1=2 /* INT meta=0 nullable=0 is_null=0 */
###   @2=2 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=2 /* INT meta=0 nullable=1 is_null=0 */

#查看自增值的变化
mysql> show create table t_replaceinto\G
*************************** 1. row ***************************
       Table: t_replaceinto
Create Table: CREATE TABLE `t_replaceinto` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4

从结果得出:唯一键值不变,自增主键+1。从"2 rows affected"可知,此操作逻辑上相当于delete+insert操作,但binlog实际记录为1次update操作。

情况3:主键冲突 + 唯一键不冲突

#插入id=1的记录,让主键冲突
mysql> replace into t_replaceinto(id, c) values(1, 11);
Query OK, 2 rows affected (0.01 sec)

#查看相关binlog
### DELETE FROM `modb`.`t_replaceinto`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### INSERT INTO `modb`.`t_replaceinto`
### SET
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=11 /* INT meta=0 nullable=1 is_null=0 */

从结果得出:先删除主键冲突的记录,再插入新记录。从"2 rows affected"可知,此操作相当于delete+insert操作,binlog中记录为delete和insert操作。

情况4:主键冲突 + 唯一键冲突(同一行)

#再插入1条新数据测试
mysql> insert into t_replaceinto(id, c) values(9, 9);
Query OK, 1 row affected (0.00 sec)

#让主键和唯一键均冲突
mysql> replace into t_replaceinto(id, c) values(9, 9);
Query OK, 2 rows affected (0.01 sec)

#查看相关binlog
### DELETE FROM `modb`.`t_replaceinto`
### WHERE
###   @1=9 /* INT meta=0 nullable=0 is_null=0 */
###   @2=9 /* INT meta=0 nullable=1 is_null=0 */
### INSERT INTO `modb`.`t_replaceinto`
### SET
###   @1=9 /* INT meta=0 nullable=0 is_null=0 */
###   @2=9 /* INT meta=0 nullable=1 is_null=0 */

从结果得出:同情况3

情况5:主键冲突 + 唯一键冲突(两行)

#查看当前数据
mysql> select * from t_replaceinto;
+----+------+
| id | c    |
+----+------+
|  3 |    2 |
|  9 |    9 |
|  1 |   11 |
+----+------+
3 rows in set (0.00 sec)

#让id=9和c=11两行记录均冲突
mysql> replace into t_replaceinto(id, c) values(9, 11);
Query OK, 3 rows affected (0.02 sec)

#查看相关binlog
### DELETE FROM `modb`.`t_replaceinto`
### WHERE
###   @1=9 /* INT meta=0 nullable=0 is_null=0 */
###   @2=9 /* INT meta=0 nullable=1 is_null=0 */
### UPDATE `modb`.`t_replaceinto`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=11 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=9 /* INT meta=0 nullable=0 is_null=0 */
###   @2=11 /* INT meta=0 nullable=1 is_null=0 */

从结果得出:先按主键delete冲突记录,再按唯一键update冲突记录。注意,影响行数为:“3 rows affected”。

现总结如下:

  1. 当表中自增主键和唯一键都存在时:
  • 主键不冲突 + 唯一键不冲突,相当于insert。
  • 主键不冲突 + 唯一键冲突,update唯一键冲突的记录,且自增主键值+1
  • 主键冲突 + 唯一键不冲突,先删除主键冲突的记录,再插入新记录。
  • 主键冲突 + 唯一键冲突(同一行),先删除主键冲突的记录,再插入新记录。
  • 主键冲突 + 唯一键冲突(两行),先删除主键冲突的记录,再update唯一键冲突记录。
  1. 当表中只存在主键时(这个大家自行测试):
  • 主键冲突,相当于update操作
  • 主键不冲突,相当于insert操作

从实验和总结中可以看出:主键冲突时,基本规则是先delete再insert记录;唯一键冲突时,基本规则是进行update操作,但自增主键值会更新为当前的AUTO_INCREMENT值。

replace into主从切换后主键冲突

从上面的测试可以看出,replace into语法可能导致自增主键的值发生变化,由于binlog一般都使用ROW模式,所以主从数据同步是正常的。但这儿会有一个潜在的问题,那就是:表的AUTO_INCREMENT值主从可能会不一致。当主从因故障切换后,从库再次插入数据时,生成的主键自增id,就有可能与已存在的主键值冲突。测试如下:

#主库:插入3条记录,并在最后1条记录的唯一键上做replace into
mysql> truncate table t_replaceinto;
Query OK, 0 rows affected (0.04 sec)

mysql> replace into t_replaceinto(c) values(1),(2),(3);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> replace into t_replaceinto(c) values(3);
Query OK, 2 rows affected (0.00 sec)

#主库上的AUTO_INCREMENT=5
mysql> show create table t_replaceinto\G
*************************** 1. row ***************************
       Table: t_replaceinto
Create Table: CREATE TABLE `t_replaceinto` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> select * from t_replaceinto;
+----+------+
| id | c    |
+----+------+
|  1 |    1 |
|  2 |    2 |
|  4 |    3 |
+----+------+
3 rows in set (0.00 sec)

#从库:复制正常,但表的AUTO_INCREMENT比主库小1
mysql> select * from t_replaceinto;
+----+------+
| id | c    |
+----+------+
|  1 |    1 |
|  2 |    2 |
|  4 |    3 |
+----+------+
3 rows in set (0.00 sec)

#从库上的AUTO_INCREMENT=4
mysql> show create table t_replaceinto\G
*************************** 1. row ***************************
       Table: t_replaceinto
Create Table: CREATE TABLE `t_replaceinto` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

#当在从库插入1条新记录时,报主键冲突。因为按最新AUTO_INCREMENT生成的id值已经存在。
mysql> insert into t_replaceinto(c) values(999);
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'

#或者:用replace into更新一条旧记录时,也报主键冲突。
mysql> insert into t_replaceinto(c) values(2);
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'

从上面结果可以得出:replace into语法在自增主键+唯一键的情况下,可能会导致主从AUTO_INCREMENT值不一致。从而主从切换后,造成自增主键冲突。

知识总结

  1. mysql5.7版本,如果表存在自增主键和唯一键时,当使用replace into语法处理唯一键冲突时,会导致主从的AUTO_INCREMENT值不一致,当主从切换后,从库可能出现唯一键冲突。
  2. mysql8.0版本,由于持久化了AUTO_INCREMENT值,故AUTO_INCREMENT值可以同步至从库,故不存在这个问题。