写点什么

记一场 DM 同步引发的 Auto_Increment 主键冲突漫谈

  • 2022 年 7 月 11 日
  • 本文字数:2314 字

    阅读完需:约 8 分钟

作者: 代晓磊 _Mars 原文来源:https://tidb.net/blog/ce4e4dd6

记一场 DM 同步引发的 Auto_Increment 主键冲突漫谈

问题描述

最近在进行 MySQL->TiDB 的迁移,大家正常的迁移主要流程都是:


  1. 通过 DM 同步 mysql 的数据到 TiDB,将 TiDB 作为 mysql 的 slave。

  2. 切读流量到 TiDB。

  3. 停 DM 同步。

  4. 切写流量到 TiDB。


主要流程虽然简单,但是忽略细节会导致一些问题产生。我们的问题就产生在上面的第三步骤:将写流量切到 TIDB,开发反馈我们每张表都遇到了 Duplicated Error 错误。

问题思考

我们每张表都有自增 ID 主键,并且业务没有显示指定主键 ID 值写入,按说 TiDB 按照 AUTO_INCREMENT 给新写入的值自动赋值,为啥能有 ID 主键冲突?难道有 AUTO_INCREMENT 有 bug?看来有必要对 TiDB 的 AUTO_INCREMENT 进行一次详细的调研了。

问题调研

TiDB 自增主键的分配规则


  1. 每个表都只能有一个 AUTO_INCREMENT,并且自增 id 按照 id 段 (默认 3 万一个区间,可配置) 提前预分配给所有 tidb server。举例说明:2 个 TiDB server,当表刚创建时,TiDB1 缓存[1,30000],TiDB2 缓存[30001,60000],这样 2 个 Session 分别连接到 2 个 TiDB,每个都写入一条记录的话,一个 id=1,一个 id=30001。

  2. 同一个表在同一个 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 来更新缓存,再次写入就不会有问题了,但是如果业务程序没有冲突重试机制的话,数据就写丢了。


注意上面的流程如果结合悲观事务和乐观事务会有区别:


  1. 2 个 Session 都是乐观事务,则是上面的报错。

  2. 悲观事务和乐观模式混用 (不会有主键冲突):

  3. 比如 Session1 开启悲观事务 (显示事务),Session2(乐观事务) 写入 id=2 的记录,Session1 写入一条不带 id 的记录,可以写入成功,Session1 获取的 id 是 3。

  4. 2 个 Session 都是悲观事务(中间会有等待,不会主键冲突)

  5. 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 处理自增主键热点表,适用于代替自增主键,解决自增主键带来的写入热点。


发布于: 刚刚阅读数: 2
用户头像

TiDB 社区官网:https://tidb.net/ 2021.12.15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
记一场DM同步引发的Auto_Increment主键冲突漫谈_故障排查/诊断_TiDB 社区干货传送门_InfoQ写作社区