pt-ost添加唯一索引会丢数据
[TOC]
准备知识
大家都知道,如果要给某个字段上加上一个唯一索引,可以这么执行:
ALTER TABLE t1 ADD UNIQUE udx_c1 (c1);
如果这个字段上有重复数据,则会报错如下:
ERROR 1062 (23000): Duplicate entry 'aaa' for key 'udx_c1'
这就意味着,MySQL的唯一索引,同时起到了唯一约束的作用,这个作用体现在两个方面:
- 在有唯一索引的字段,不能插入跟已经存在的数据重复的行;
- 在有重复行的字段上,不能创建唯一索引。
这个还是比较好理解的,然后你就会对这篇文章的标题表示疑问:既然是约束,大不了在创建唯一索引的时候就失败好了,怎么会导致丢数据呢?
这个问题要从加索引的代价说起。
加索引的代价
在MySQL的老版本里,由于加索引和加字段是会锁表的,这个期间表上不能更新,因此不能直接在业务库上操作。不过从5.6版本开始,MySQL已经支持了online DDL特性,也就是在加索引和加字段的过程中,这个表是可以正常读写的。
虽然不会影响读写业务,但是在大表是加索引还存在另一个问题,就是会导致主备延迟。如果添加索引在主库上执行了30分钟,那么这个事务传到从库,也要执行30分钟,这样就会导致从库有30分钟的延迟。
如果是用的MySQL5.6及之后的版本,并且操作的是小表,由于不会导致明显的延迟,是可以直接在线执行的。当然还是建议在低峰期执行这个操作。
pt-ost加索引
那你会问了,如果是大表加索引,怎么解决这个延迟的问题呢?这时候就要介绍到在线加字段的工具了,比如percona的工具pt-ost(其实更常用的还有gh-ost,这里先不展开)
先介绍一下 pt-ost的流程,假设要给前面的t1表的c1字段上加一个唯一索引,执行流程是这样的:
- 创建一个临时表,假设为t2,t2和t1的表结构一样,由于t2是一个空表,所以在c1字段上创建唯一索引是肯定成功的
- 从t1表中一行行读出数据,写入到t2表中,这个过程称为全量阶段
- 如果在全量阶段中,t1表上有更新数据,会用触发器写入到t2中,这个过程称为增量阶段
- 增量阶段完成后,t1和t2交换表名,用户看到的t1表就是加完索引的t2表,然后再把新的t2表(也就是老的t1表)删掉
这个工具把一个加索引的大操作,改成了几个小操作来执行,可以解决主备延迟的问题。
不过你一定发现了,全量阶段有问题。t2表c1字段已经有了唯一索引,在插入第一行数据(1,1)的时候是没问题的,但是插入第二行数据(2,1)的时候,就违反了唯一索引的约束,插入语句会报错。
下图的状态2表示了这个过程。
但是,pt工具会忽略这个错误,继续执行后面的流程,这样全量阶段结束后,t2表就少了一行,交换表名后,用户就会看到t1表丢了数据。
pt-ost的check-unique-key-change参数
这么严重的一个bug,官方当然还是要处理的,在 pt-ost 3.0 版本里面,增加了一个参数 --check-unique-key-change
。看这个参数名的字面意思,好像是在加唯一索引之前会判断一下这个操作是否会丢数据。
实际上是不是这样呢?来看一个例子。假设现在在一个空表上用pt-ost工具加一个唯一索引,执行的命令如下:
./pt-online-schema-change D=test,t=t1 --execute --alter "ADDUNIQUE udx_c1(c1)"
这个命令的意思是,在test库的t1表的c1字段上,创建一个唯一索引,这里省略了连接信息。虽然现在这个表是空表,但执行这个命令仍然会报错,报错信息如下
Altering `test`.`t1` ...
`test`.`t1` 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) contaion duplicate content by running this/these query/queries:
输出的提示里说明,发现你要创建一个唯一索引,所以工具退出。而如果在上面的命令中增加一个参数 --nocheck-unique-key-change
,这个命令就能执行成功。
这时候你一定有些怀疑了,这个参数看来并没有真的去检查数据是否重复?你一定很容易设计一个实验,在test库的t1表插入两行完全相同的数据,然后执行pt-ost工具,并且带上参数--nocheck-unique-key-change
。这时候命令能够成功执行,并且丢了一行数据。
有了上面这三个对照实验,你现在知道了这个参数的设计逻辑:pt-ost工具知道它在创建唯一索引的时候,可能会导致数据丢失。因此默情况下,只要发现你要用这个工具来创建唯一索引,就直接报错返回。
而使用这个功能的正确姿势如下:
- 1、首先要先确保要加唯一索引的字段上没有重复值。这个判断逻辑,工具并不会帮你做,但是在提示中给了方法,就是用 count(distinct(c1)) 的值来判断。
如果值等于表的行数,表示没有重复值;
如果值小于表的行数,表示有重复值;
- 2、有重复值就不要创建唯一索引了;
- 3、如果确认没有重复值,就明确带上
--nocheck-unique-key-change
这个参数,来执行创建唯一索引的命令。
小结
总结一下此文的主要内容:
- MySQL的InnoDB表加普通索引和唯一索引的时候,支持在线加索引,不会阻塞线上的读写操作;
- 如果表不大,可以在几分钟之内执行完成加索引操作,可以在低峰期直接加,效果更快;
- 直接在表上加唯一索引是没有风险的,如果字段上存在重复值,加索引语句会报错;
- 如果要用pt-ost工具加唯一索引,要主动先确认要加唯一索引的字段上没有重复值,否则可能会导致丢数据。