MySQL 创建唯一索引会导致数据丢失吗?

引言

MySQL 中创建唯一索引是再正常不过的需求,但是有人说创建唯一索引可能导致数据丢失,是真的吗?

本文首先分别在有数据重复的前提下,验证这三种执行方式创建唯一索引时是否会导致数据丢失,然后通过介绍相关原理进行解释。

最终发现导致数据丢失的原因是 DDL 执行工具改写原始 SQL,而原生 online ddl 并不会导致数据丢失。

分析

唯一索引

首先,通过测试介绍唯一索引的作用。

创建表并插入数据,其中有唯一索引。

mysql> create table tt(
    -> id int primary key auto_increment,
    -> a int default 0,
    -> b int default 0,
    -> unique key uk_a(a)
    -> );
Query OK, 0 rows affected (0.02 sec)

mysql> insert into tt(a,b) values(1,1),(2,1);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

然后插入重复数据,直接报错

mysql> insert into tt(a,b) values(2,1);
ERROR 1062 (23000): Duplicate entry '2' for key 'uk_a'

如果给字段 b 创建唯一索引,同样报错。

mysql> alter table tt add unique key uk_b(b);
ERROR 1062 (23000): Duplicate entry '1' for key 'uk_b'

可见,唯一索引的作用包括:

  • 在有唯一索引的字段,不能插入跟已经存在的数据重复的行;
  • 在有重复行的字段上,不能创建唯一索引。

下面介绍为什么创建唯一索引可能导致数据丢失,实际上这与 DDL 的执行方式有关。

DDL

DDL 常见的执行方式包括:

  • online ddl
  • pt-osc
  • gh-ost

尽管执行方式有多种,但基本流程是类似的,都是新建表或临时文件,然后分别同步存量数据与增量数据,区别在于具体的实现方式。

那么数据丢失是什么概念呢?

数据丢失表现为原表中写入成功,但是创建唯一索引后新表中查不到了。

同步存量数据与增量数据时都有可能导致数据丢失,因此需要分开讨论。

下面首先分别验证在有数据重复的前提下,这三种执行方式创建唯一索引时是否会导致数据丢失,然后通过介绍相关原理进行解释。

测试

准备数据

表结构,其中有唯一索引。

mysql> show create table t G
*************************** 1. row ***************************
       Table: t
Create TableCREATE TABLE `t` (
  `id` int(10NOT NULL AUTO_INCREMENT,
  `a` int(10DEFAULT NULL,
  `name` varchar(20DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_a` (`a`)
ENGINE=InnoDB AUTO_INCREMENT=3721491 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

数据量,不到 400w 行数据。

mysql> show table status like 't' G
*************************** 1. row ***************************
           Name: t
         EngineInnoDB
        Version10
     Row_format: Dynamic
           Rows3941727
 Avg_row_length: 32
    Data_length: 128598016
Max_data_length: 0
   Index_length: 54099968
      Data_free: 57671680
 Auto_increment: 4044177
    Create_time: 2023-03-17 15:09:44
    Update_time: 2023-03-17 15:20:39
     Check_time: NULL
      Collation: utf8mb4_general_ci
       ChecksumNULL
 Create_options: 
        Comment
1 row in set (0.00 sec)

测试数据

mysql> select * from t limit 3;
+----+------+------+
| id | a    | name |
+----+------+------+
|  1 |    1 | test |
|  2 |    2 | test |
|  3 |    3 | test |
+----+------+------+
3 rows in set (0.00 sec)

删除已有的唯一索引,用于后续测试创建唯一索引。

mysql> alter table t drop key uk_a;
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

关于删除索引介绍两点特性:

  • 删除索引是秒级操作,原因是删除索引属于仅修改元数据;
  • 删除索引并不会释放空间,重建表以后才可以回收空间。

online ddl

存量数据,直接报错。

mysql> update t set a=2 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> alter table t add unique key uk_a(a);
ERROR 1062 (23000): Duplicate entry '2' for key 'uk_a'

增量数据,插入未报错,DDL 执行结束时报错。

session 1 session 2
alter table t add unique key uk_a(a);

mysql> insert into t(a,name) values(1,’t’);
Query OK, 1 row affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uk_a’

其中:

  • 连接2中在 DDL 开始后写入重复数据,未报错;
  • 连接1中 DDL 执行完成后报错数据重复。


DDL 执行前后查询数据表,显示尽管 DDL 执行失败,但是插入的重复数据并没有丢失

MySQL 创建唯一索引会导致数据丢失吗?

pt-osc

命令

pt-online-schema-change 
--host=127.0.0.1 --port=3358 --user=admin --password=""  
--charset=utf8 --no-check-replication-filters --print --statistics 
--recursion-method=processlist,hosts --recurse=1 
--critical-load="Threads_running=99999"  
--progress percentage,10 --execute 
D='cctest',t='t' --alter "add unique key uk_a(a);"

测试存量数据

session 1 session 2
mysql> update t set a=2 where id=1;
Query OK, 1 row affected (0.01 sec)


pt-online-schema-change
cctest.t was not altered.
You are trying to add an unique key.

详细报错

No slaves found.  See --recursion-method if host test has slaves.
Not checking slave lag because no slaves were found and --check-slave-lag was not specified.
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `cctest`.`t`...
# Event  Count
# ====== =====
# INSERT     0
`cctest`.`t` was not altered.
You are trying to add an unique key. This can result in data loss if the data is not unique.
Please read the documentation for the --check-unique-key-change parameter.
You can check if the column(s) contain duplicate content by running this/these query/queries:

SELECT IF(COUNT(DISTINCT a) = COUNT(*),
       'Yes, the desired unique index currently contains only unique values'
       'No, the desired unique index contains duplicated values. There will be data loss'
AS IsThereUniqueness FROM `cctest`.`t`;

Keep in mind that these queries could take a long time and consume a lot of resources

实际上,如果指定--nocheck-unique-key-change参数,pt-osc 可以强制执行,执行日志如下所示。

No slaves found.  See --recursion-method if host test has slaves.
Not checking slave lag because no slaves were found and --check-slave-lag was not specified.
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `cctest`.`t`...
Creating new table...
CREATE TABLE `cctest`.`_t_new` (
  `id` int(10NOT NULL AUTO_INCREMENT,
  `a` int(10DEFAULT NULL,
  `name` varchar(20DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=5616978 DEFAULT CHARSET=utf8mb4
Created new table cctest._t_new OK.
Altering new table...
ALTER TABLE `cctest`.`_t_new` add unique key uk_a(a);
Altered `cctest`.`_t_new` OK.
2023-03-17T17:25:27 Creating triggers...
2023-03-17T17:25:38 Created triggers OK.
2023-03-17T17:25:38 Copying approximately 5126726 rows...
INSERT LOW_PRIORITY IGNORE INTO `cctest`.`_t_new` (`id``a``name`SELECT `id``a``name` FROM `cctest`.`t` FORCE INDEX(`PRIMARY`WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 18406 copy nibble*/
SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `cctest`.`t` FORCE INDEX(`PRIMARY`WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chunk boundary*/
Copying `cctest`.`t`:  1100:31 remain
Copying `cctest`.`t`:  2000:23 remain
Copying `cctest`.`t`:  3000:22 remain
Copying `cctest`.`t`:  4100:18 remain
Copying `cctest`.`t`:  5000:14 remain
Copying `cctest`.`t`:  6100:11 remain
Copying `cctest`.`t`:  7000:08 remain
Copying `cctest`.`t`:  8100:05 remain
Copying `cctest`.`t`:  9000:02 remain
2023-03-17T17:26:10 Copied rows OK.
2023-03-17T17:26:10 Analyzing new table...
2023-03-17T17:26:10 Swapping tables...
RENAME TABLE `cctest`.`t` TO `cctest`.`_t_old``cctest`.`_t_new` TO `cctest`.`t`
2023-03-17T17:26:10 Swapped original and new tables OK.
2023-03-17T17:26:10 Dropping old table...
DROP TABLE IF EXISTS `cctest`.`_t_old`
2023-03-17T17:26:10 Dropped old table `cctest`.`_t_old` OK.
2023-03-17T17:26:10 Dropping triggers...
DROP TRIGGER IF EXISTS `cctest`.`pt_osc_cctest_t_del`
DROP TRIGGER IF EXISTS `cctest`.`pt_osc_cctest_t_upd`
DROP TRIGGER IF EXISTS `cctest`.`pt_osc_cctest_t_ins`
2023-03-17T17:26:10 Dropped triggers OK.
# Event              Count
# ================== =====
# INSERT                62
# mysql_warning_1062     1
Successfully altered `cctest`.`t`.

但实际上,如果查看数据表,会发现重复数据丢失

MySQL 创建唯一索引会导致数据丢失吗?

唯一键a=2对应两条记录,其中id=1的列保留,id=2的列数据丢失,原因是存量数据同步使用的是 insert ignore 语句,因此丢失后面的记录


测试增量数据,同样数据丢失。

session 1 session 2

pt-online-schema-change –nocheck-unique-key-change
mysql> insert into t(a,name) values(2,’t’);
Query OK, 1 row affected (0.00 sec)

执行前后分别查询数据表。

MySQL 创建唯一索引会导致数据丢失吗?

唯一键a=2对应两条记录,其中id=1的列丢失,id=5616982的列数据丢失,原因是增量数据同步使用的是 replace into 语句,因此丢失后面的记录

注意:

  • insert 时不可以开启事务,否则将导致DDL等待元数据锁;
  • pt-osc 不建议指定--nocheck-unique-key-change参数,否则存量数据与增量数据中的重复数据都有可能丢失。

gh-ost

命令

gh-ost 
--max-load=Threads_running=50 
--critical-load=Threads_running=500,Threads_connected=800 
--chunk-size=1000 
--max-lag-millis=1000 
--port=3358 
--user="admin" 
--password="" 
--assume-rbr 
--allow-on-master 
--concurrent-rowcount 
--panic-flag-file="ghost.panic.flag" 
--serve-socket-file="ghost.sock" 
--host="127.0.0.1" 
--assume-master-host="127.0.0.1:3358" 
--database="cctest" 
--table="t" 
--alter="add unique key uk_a(a);" 
--timestamp-old-table 
--execute 

其中:

  • –max-lag-millis int:主从复制最大延迟时间,当主从复制延迟时间超过该值后,gh-ost 将采取节流 (throttle) 措施,默认值:1500s;
  • –assume-rbr:确认 gh-ost 连接的数据库实例的 binlog_format=ROW 的情况下,可以指定 -assume-rbr,这样可以禁止从库上运行 stop slave,start slave,执行 gh-ost 用户也不需要 SUPER 权限;
  • –allow-on-master:允许 gh-ost 直接运行在主库上。默认 gh-ost 连接的从库;
  • –panic-flag-file string:当这个文件被创建,gh-ost 将会立即退出;
  • –serve-socket-file string:gh-ost 的 socket 文件绝对路径;
  • –assume-master-host string:为 gh-ost 指定一个主库,格式为 ”ip:port” 或者 ”hostname:port”。在这主主架构里比较有用,或则在 gh-ost 发现不到主的时候有用;
  • –timestamp-old-table:在旧表名中使用时间戳。这会使旧表名称具有唯一且无冲突的交叉迁移。


同样首先测试存量数据

session 1 session 2
mysql> update t set a=2 where id=1;
Query OK, 1 row affected (0.01 sec)


gh-ost

执行日志

# Migrating `cctest`.`t`; Ghost table is `cctest`.`_t_gho`
# Migrating test:3358; inspecting test:3358; executing on test
# Migration started at Mon Mar 20 11:16:47 +0800 2023
# chunk-size: 1000; max-lag-millis: 1000ms; dml-batch-size: 10; max-load: Threads_running=50; critical-load: Threads_connected=800,Threads_running=500; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle 
# panic-flag-file: ghost.panic.flag
# Serving on unix socket: ghost.sock
Copy: 5616964/5616964 100.0%; Applied: 0; Backlog: 0/1000; Time: 54s(total), 53s(copy); streamer: mysql-bin.000019:407616865; Lag: 0.01s, HeartbeatLag: 0.06s, State: migrating; ETA: due
[2023/03/20 11:17:41] [info] binlogsyncer.go:164 syncer is closing...
[2023/03/20 11:17:41] [error] binlogstreamer.go:77 close sync with err: sync is been closing...
[2023/03/20 11:17:41] [info] binlogsyncer.go:179 syncer is closed

执行完成后同样发生数据丢失,原因是存量数据同步使用的是 insert ignore 语句,因此丢失后面的记录

其次测试增量数据

session 1 session 2

gh-ost
mysql> insert into t(a,name) values(2,’t’);
Query OK, 1 row affected (0.00 sec)

执行日志

# Migrating `cctest`.`t`; Ghost table is `cctest`.`_t_gho`
# Migrating test:3358; inspecting test:3358; executing on test
# Migration started at Mon Mar 20 12:22:01 +0800 2023
# Migration started at Mon Mar 20 12:22:01 +0800 2023
# chunk-size: 1000; max-lag-millis: 1000ms; dml-batch-size: 10; max-load: Threads_running=50; critical-load: Threads_connected=800,Threads_running=500; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle 
# throttle-additional-flag-file: /tmp/gh-ost.throttle 
# panic-flag-file: ghost.panic.flag
# Serving on unix socket: ghost.sock
# panic-flag-file: ghost.panic.flag
# Serving on unix socket: ghost.sock
Copy: 5616965/5616965 100.0%; Applied: 1; Backlog: 0/1000; Time: 55s(total), 54s(copy); streamer: mysql-bin.000019:490087050; Lag: 0.01s, HeartbeatLag: 0.08s, State: migrating; ETA: due
[2023/03/20 12:22:57] [info] binlogsyncer.go:164 syncer is closing...
[2023/03/20 12:22:57] [error] binlogstreamer.go:77 close sync with err: sync is been closing...
[2023/03/20 12:22:57] [info] binlogsyncer.go:179 syncer is closed
# Done

执行完成后同样发生数据丢失,原因是增量数据同步使用的是 replace into 语句,因此丢失后面的记录

原理

测试结果显示创建唯一索引时,online ddl 不会导致数据丢失,但是 pt-osc 与 gh-ost 都有可能导致数据丢失。

实际上,根本原因在于数据迁移过程中数据插入的方式不同。下面进行详细讲解。

online ddl

online ddl 的原理如下图所示,注意下图是重建表的方式,创建索引并不会重建表,因此存在差异。

MySQL 创建唯一索引会导致数据丢失吗?

执行流程:

  • 创建临时文件,用于保存索引;
  • 同步全量数据。遍历主键索引,将对应的字段(多字段)值,写到新索引;
  • 同步增量数据。遍历期间将修改记录保存到 row log 中,遍历完成后回放 row log。因此 DDL 过程中不阻塞 DML,称为 online ddl;
  • 由于 Rebuilds Table=NO,因此不存在 rename table、drop origin_table 的环节。


根据测试结果,online ddl 会校验重复数据,如果发现唯一键冲突,将直接退出,不会导致数据丢失。

那么,创建唯一索引时,如何校验重复数据?

实际上,online ddl 创建唯一索引的过程中包括 Check Duplicate Key 环节,其中在 row log 回放过程中存储遇到的违反唯一性约束的 row log。在应用完 row log 以后,外部判断是否存在 unique 冲突(有多少 unique 冲突,均会记录),如果有冲突,online 创建 unique 索引失败。

因此 online ddl 过程中可以将全量数据与增量数据(insert)的同步全部理解为原本的 insert。


注意 online ddl 期间如果业务写入非常频繁,可能导致执行报错ERROR 1799 (HY000) at line 150: Creating index 'PRIMARY' required more than 'innodb_online_alter_log_max_size' bytes of modification log.。原因是操作期间的DML(增删改)操作超过临时日志文件大小,默认 128M。

pt-osc

pt-osc 的原理如下图所示。

MySQL 创建唯一索引会导致数据丢失吗?
percona-toolkit-for-effective-mysql-administration

执行流程:

  • 创建新表与原表的表结构一致,然后添加唯一索引;
  • 同步全量数据,遍历全表,通过INSERT LOW_PRIORITY IGNORE INTO语句将数据拷贝到新表;
  • 同步增量数据,通过触发器在原表写入时同时写入新表,分别创建以下三个触发器。
操作类型 语法
INSERT REPLACE INTO
DELETE DELETE IGNORE
UPDATE DELETE IGNORE + REPLACE INTO
  • 执行完成后交换表并删除原表。


因此,pt-osc 不会校验数据的重复值,数据重复时会导致数据丢失:

  • 全量数据,直接忽略,后面的数据丢失;
  • 增量数据,直接覆盖,前面的数据丢失。

pt-ost 3.0 版本中新增参数 check-unique-key-change 用于控制是否允许强制创建唯一索引,默认开启。

可以通过指定--nocheck-unique-key-change参数强制创建唯一索引。

可以发现,导致数据丢失的原因是 pt-osc 将 INSERT 语句转换成了INSERT IGNOREREPLACE INTO语句。


触发器是 pt-osc 的核心,因此触发器的表达式非常重要

执行中查看触发器,其中可以看到创建触发器的 SQL。

mysql> show triggers G
*************************** 1. row ***************************
             Trigger: pt_osc_cctest_t_ins
               EventINSERT
               Table: t
           StatementREPLACE INTO `cctest`.`_t_new` (`id``a``name`VALUES (NEW.`id`, NEW.`a`, NEW.`name`)
              Timing: AFTER
             Created: 2023-03-23 17:57:42.74
            sql_mode: NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
             Definer: admin@%
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: utf8mb4_general_ci
*************************** 2. row ***************************
             Trigger: pt_osc_cctest_t_upd
               EventUPDATE
               Table: t
           StatementBEGIN DELETE IGNORE FROM `cctest`.`_t_new` WHERE !(OLD.`id` <=> NEW.`id`AND `cctest`.`_t_new`.`id` <=> OLD.`id`;REPLACE INTO `cctest`.`_t_new` (`id``a``name`VALUES (NEW.`id`, NEW.`a`, NEW.`name`);END
              Timing: AFTER
             Created: 2023-03-23 17:57:42.73
            sql_mode: NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
             Definer: admin@%
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: utf8mb4_general_ci
*************************** 3. row ***************************
             Trigger: pt_osc_cctest_t_del
               EventDELETE
               Table: t
           StatementDELETE IGNORE FROM `cctest`.`_t_new` WHERE `cctest`.`_t_new`.`id` <=> OLD.`id`
              Timing: AFTER
             Created: 2023-03-23 17:57:42.73
            sql_mode: NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
             Definer: admin@%
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: utf8mb4_general_ci
3 rows in set (0.00 sec)

MySQL 中定义了 NEW 和 OLD,用来表示触发器的所在表中,触发了触发器的那一行数据。其中:

  • 在 INSERT 型触发器中,NEW 用来表示将要(BEFORE)或已经(AFTER)插入的新数据;
  • 在 UPDATE 型触发器中,OLD 用来表示将要或已经被修改的原数据,NEW 用来表示将要或已经修改为的新数据;
  • 在 DELETE 型触发器中,OLD 用来表示将要或已经被删除的原数据;

那么,为什么 pt-osc 同步存量与增量数据时需要转换原始 SQL 呢?

根本原因是 pt-osc 执行时同一行数据对应的存量数据与增量数据的执行顺序不确定

为了在执行顺序不确定的场景下保证数据的一致性,pt-osc 将存量数据与增量数据同步的 SQL 进行了转换,具体如下:

  • insert 语句:

    • 如果数据写入时还没拷贝新表,先REPLACE INTO写入,INSERT IGNORE拷贝跳过;
    • 如果拷贝到新表后数据写入,REPLACE INTO覆盖写入。
  • update 语句:

    • 如果数据写入时还没拷贝新表,先REPLACE INTO写入,INSERT IGNORE拷贝跳过;
    • 如果拷贝到新表后数据写入,判断新旧主键ID是否一致,如果一致先DELETE IGNORE删除后REPLACE INTO写入。
  • delete 语句:

    • 如果数据删除时还没拷贝新表,先DELETE IGNORE删除,后SELECT查不到数据因此不存在拷贝;
    • 如果拷贝到新表后数据删除,直接DELETE IGNORE删除。


那么,pt-osc 中为什么存量数据与增量数据的执行顺序不确定呢?

实际上,存量与增量执行顺序不确定表明存量与增量可以“同时”执行,而在主库写入 binlog 以后从库也就可以执行了,因此可以认为主从“同时”执行,进而减小主从延迟,这也是 pt-osc 工具的核心作用。

对比 online ddl,从库需要在主库执行完成后开始执行,因此天然存在主从延迟。


此外,拷贝数据的 SQL 如下所示,显示拷贝过程使用的是语句INSERT LOW_PRIORITY IGNORE INTO LOCK IN SHARE MODE ,表明拷贝过程对数据行持有 S 锁。

INSERT LOW_PRIORITY IGNORE INTO `cctest`.`_t_new` (`id``a``name`SELECT `id``a``name` FROM `cctest`.`t` FORCE INDEX(`PRIMARY`WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change 18406 copy nibble*/

那么,拷贝过程中为什么需要 S 锁呢?

我们知道,读操作可以分为快照读与当前读:

  • 快照读,一致性读,Consistent Read,普通的 select 语句不会对记录加锁,通过 MVCC 实现;
  • 锁定读,当前读,Locking Read,读取过程中对记录加锁:
    • SELECT … LOCK IN SHARE MODE,加 S 锁,可读不可写;
    • SELECT … FOR UPDATE,加 X 锁,不可读写。

可见,pt-osc 拷贝存量数据期间通过加 S 锁不允许写入,下面分析原因。

  • insert 语句,不相关;
  • update 语句,如果不加锁:
    • 假设先读取再写入最后拷贝,最终的数据是写入的,因为拷贝跳过;
    • 假设先写入再读取最后拷贝,最终的数据是也写入的,因为拷贝跳过。
  • delete 语句,如果不加锁:
    • 假设先读取再删除最后拷贝,最终的数据没有被删除,因为读取到了老数据;
    • 假设先删除再读取最后拷贝,最终的数据也可能没有被删除,如果是快照读。

因此,拷贝过程中需要加 S 锁保证没有写入,也正是如此,执行 pt-osc 可能导致业务 SQL 锁等待。

gh-ost

gh-ost 的原理如下图所示。

MySQL 创建唯一索引会导致数据丢失吗?

执行流程:

  • 创建一张与原表结构一致的新表,然后添加唯一索引;
  • 同步全量数据。遍历原表,通过INSERT IGNORE INTO将数据拷贝到新表;
  • 同步增量数据。通过应用原表 DML 产生的 binlog 同步增量数据。
操作类型 语法
INSERT REPLACE INTO
DELETE DELETE
UPDATE UPDATE
  • 执行完成后交换表并删除原表。


从中可以发现:

  • pt-osc 与 gh-ost 的区别在于增量数据同步方式的实现,前者依赖触发器,后者依赖 binlog;
  • pt-osc 与 gh-ost 的相同点是都将增量与全量同步中的 INSET 语句转换成了INSERT IGNOREREPLACE INTO语句。但是 UPDATE 与 DELETE 转换后的语法有差异,具体原因待后续分析;
  • gh-ost 中也没有校验数据的重复值,因此同样可能导致数据丢失,如果是全量同步,直接忽略,如果是增量同步,直接覆盖。但是 gh-ost 提供了 hook 功能可以进行辅助,比如:
    • 改表前执行脚本检查存量数据中是否有重复值,如果有,直接退出添加唯一索引;
    • 交换表前执行脚本检查唯一索引的字段


拷贝数据的 SQL 如下所示,也是INSERT LOW_PRIORITY IGNORE INTO LOCK IN SHARE MODE

insert /* gh-ost `cctest`.`t` */ ignore into `cctest`.`_t_gho` (`id``a``name`)
      (select `id``a``name` from `cctest`.`t` force index (`PRIMARY`)
        where (((`id` > _binary'4055001')) and ((`id` < _binary'4056001'or ((`id` = _binary'4056001')))) lock in share mode
      )

pt-osc 与 gh-ost 的日志差异就很大了,从日志中可以看到,gh-ost 会创建伪装从库拉取 binlog 用于实现增量同步。

MySQL 创建唯一索引会导致数据丢失吗?

通常都是从库应用 binlog,而这里 gh-ost 在主库执行时也可以通过应用 binlog 实现功能增强。

对比

首先上结论,三种执行方式是否会导致数据丢失。

执行方式 是否可能丢数据 建议
online ddl 适合小表,或者对于主从延迟不敏感的业务
pt-osc 不建议指定--nocheck-unique-key-change参数,因此不适合创建唯一索引
gh-ost 适合创建唯一索引,建议结合 hooks 功能辅助使用

可能导致数据丢失的根本原因是写入操作在不同执行方式中对应不同的 SQL,以 insert 举例。

执行方式 insert 转换 影响
online ddl 原始 INSERT 主库执行完成后从库开始执行,因此会导致主从延迟
pt-osc INSERT IGNORE + REPLACE INTO 存量同步与增量同步同时执行,主从同时执行,因此主从延迟小
gh-ost INSERT IGNORE + REPLACE INTO 存量同步与增量同步同时执行,主从同时执行,因此主从延迟小

结论

创建唯一索引并不一定导致数据丢失,具体与执行方式有关。

其中 online ddl 不会导致数据丢失,pt-osc 与 gh-ost 都可能导致数据丢失。

根本原因是 DDL 工具执行时同一行数据对应的存量数据与增量数据的执行顺序不确定。

为了在执行顺序不确定的场景下保证数据的一致性, DDL 工具会将存量数据与增量数据同步的 SQL 进行转换。

存量与增量执行顺序不确定表明存量与增量可以“同时”执行,而在主库写入 binlog 以后从库也就可以执行了,因此可以认为主从“同时”执行,进而减小主从延迟,这也是 DDL 工具的核心作用。

对比 online ddl,从库需要在主库执行完成后开始执行,因此天然存在主从延迟。

待办

  • gh-ost hooks

参考教程

  • 咖丁奇:加唯一索引怎么会导致丢数据?


https://cloud.tencent.com/developer/article/1430264

  • M

    ySQL大表添加唯一索引的总结


https://juejin.cn/post/7211072055781310501

  • p

    t-online-schema-change的原理解析与应用说明

https://www.cnblogs.com/xinysu/p/6758170.html

  • M

    SQL下使用Inplace和Online方式创建索引的教程


https://m.xp.cn/b.php/77493.html

原文始发于微信公众号(丹柿小院):MySQL 创建唯一索引会导致数据丢失吗?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/178564.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!