记一场DM同步引发的Auto_Increment主键冲突漫谈
问题描述
最近在进行MySQL->TiDB的迁移,大家正常的迁移主要流程都是:
- 通过DM同步mysql的数据到TiDB,将TiDB作为mysql的slave。
- 切读流量到TiDB。
- 停DM同步。
- 切写流量到TiDB。
主要流程虽然简单,但是忽略细节会导致一些问题产生。我们的问题就产生在上面的第三步骤:将写流量切到TIDB,开发反馈我们每张表都遇到了Duplicated Error错误。
问题思考
我们每张表都有自增ID主键,并且业务没有显示指定主键ID值写入,按说TiDB按照AUTO_INCREMENT给新写入的值自动赋值,为啥能有ID主键冲突?难道有AUTO_INCREMENT有bug?看来有必要对TiDB的AUTO_INCREMENT进行一次详细的调研了。
问题调研
TiDB自增主键的分配规则
- 每个表都只能有一个AUTO_INCREMENT,并且自增id按照id段(默认3万一个区间,可配置)提前预分配给所有tidb server。举例说明:2个TiDB server,当表刚创建时,TiDB1缓存[1,30000],TiDB2缓存[30001,60000],这样2个Session分别连接到2个TiDB,每个都写入一条记录的话,一个id=1,一个id=30001。
- 同一个表在同一个tidb server单调自增能保证,比如所以已经在TiDB1上建立的链接写入记录时id是连续的。
问题定位
通过查看文档的多个地方都有类似的描述:建议不要将缺省值和自定义值混用,若混用可能会收 Duplicated Error 的错误信息,就是说业务或者DM肯定有显示给ID自增主键赋值了,通过跟业务沟通,业务没有自己搞ID生成器,业务在mysql时就使用的默认自增。那问题原因只能归结到DM了。
DM 作为 Mysql->TiDB 的数据同步中间件,它是将 MySQL row 格式的 binlog 解析后写入下游的TiDB,既然是行格式的 binlog,肯定主键 ID 是主动赋值的,所以这次主键冲突问题的原因找到了,是DM导致的问题。但是依然需要知道为啥混用缺省和自定义 ID 会导致问题?
通过问题调研部分大家知道了每个表都是tidb server预分配id段来分配主键。下面模拟下报错的流程:
前提 2 个 TiDB server,2 个链接分别连接到这 2 个 tidb server,Session1 链接 TiDB1(缓存[1,30000]),Session2 链接 TiDB2(缓存[30001,60000])
(1)Session1 : 创建t表
mysql> CREATE TABLE t(id int PRIMARY KEY AUTO_INCREMENT, c int);
(2)Seesion1: 手动插入一条记录
mysql> INSERT INTO t© VALUES (1);
Query OK, 1 row affected (0.16 sec)mysql> select * from t;
±—±-----+
| id | c |
±—±-----+
| 1 | 1 |
±—±-----+
1 row in set (0.00 sec)
(3)Seesion2 : 给t表id显示插入2。
mysql> INSERT INTO t(id,c) VALUES (2,1);
Query OK, 1 row affected (0.00 sec)
(4)Session1: 执行 select ,并且不指定id写入一条记录,报错:
`mysql> select * from t;
±—±-----+
| id | c |
±—±-----+
| 1 | 1 |
| 2 | 1 |
±—±-----+
2 rows in set (0.00 sec)mysql> INSERT INTO t© VALUES (1);
ERROR 1062 (23000): Duplicate entry ‘2’ for key ‘PRIMARY’`
综合这个测试说明DM默认向下游的多个 TiDB Server 并发写入上游 MySQL 的变更,对于TiDB中表本身的自增id缓存段来说,当写流量切过来的时候,有很大的可能性会遇到写入冲突,但这个写入冲突只会冲突一次后,该tidb server就会获取表max id来更新缓存,再次写入就不会有问题了,但是如果业务程序没有冲突重试机制的话,数据就写丢了。
注意上面的流程如果结合悲观事务和乐观事务会有区别:
- 2 个Session 都是乐观事务,则是上面的报错。
- 悲观事务和乐观模式混用(不会有主键冲突):
比如 Session1 开启悲观事务(显示事务),Session2(乐观事务)写入 id=2 的记录,Session1 写入一条不带 id 的记录,可以写入成功,Session1 获取的 id 是 3。 - 2个Session都是悲观事务(中间会有等待,不会主键冲突)
Session2 先占用了 id=2 的主键锁,如果不 commit,Session1 写入一条不带 id 的记录也会尝试拿id=2的锁,此时Session1 等待,Session2 提交后,Session1 可以执行成功,只是 id=3 了。
问题解决
使用DM作为MySQL->TiDB的缓冲是大部分迁移都要用到的。但是 DM 作为显示的给表ID赋值就会遇到Duplicated Error 错误,所以在上面迁移步骤的(3)停DM (4)切写之间需要再加入一个步骤,那就是重启所有 tidb server,这样TiDB会根据表的max ID,重启分配ID段。这样业务再切写就不会遇到主键冲突问题了,并且 tiup 平滑重启 tidb server 应该是秒级的操作,对业务透明。
另外就是建议业务程序加入写入失败的重试机制,在咱们本文讨论的主键冲突情况下,重试即可正确写入,数据不会丢。
AUTO_INCREMENT的其他特性
1、下面讲一些关于自增主键的其他注意事项:
(1)比如之前业务在 MySQL中采用 order by id 降序来获取最新的数据,由于 TiDB 的特性,就不能根据id 来排序了,修改为根据 create_date 等时间排序(可能需要调整索引)。
(2)TiDB 表的自增ID建议设定为bigint类型(对于要存大量的数据来说),因为 tidb server 的频繁重启会导致 AUTO_INCREMENT 缓存值被快速消耗。
2、一些TiDB自增主键的特性
(1)TiDB目前不支持 ALTER TABLE 添加自增属性
(2)支持使用 ALTER TABLE 来移除自增属性
(3)再次强调:在集群中有多个 TiDB 实例时,如果表结构中有自增 ID,建议不要混用显式插入和隐式分配(即自增列的缺省值和自定义值),否则可能会破坏隐式分配值的唯一性。
3、新尝试:使用 AUTO_RANDOM 处理自增主键热点表
对于自增主键遇到的写入热点问题,可以用 AUTO_RANDOM 处理自增主键热点表,适用于代替自增主键,解决自增主键带来的写入热点。