记一场 DM 同步引发的 Auto_Increment 主键冲突漫谈
作者: 代晓磊 _Mars 原文来源:https://tidb.net/blog/ce4e4dd6
记一场 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 处理自增主键热点表,适用于代替自增主键,解决自增主键带来的写入热点。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/ae6e71a2b529b20fc6928845d】。文章转载请联系作者。
评论