最近看到这样的一个面试题。MySQL的自增ID用完了,怎么办?以下是这个面试题的解决方案。
如果你用过或了解过MySQL,那你一定知道自增主键了。每个自增id都是定义了初始值,然后按照指定步长增长(默认步长是1)。虽然,自然数是没有上限的,但是我们在设计表结构的时候,通常都会指定字段长度,那么,这时候id就有上限了。既然有上限,就总有被用完的时候,如果id用完了,怎么办呢?今天就一起来学习下吧。
自增id
说到自增id,相信你的第一反应一定是在设计表结构的时候自定义一个自增id字段,那么就有一个问题啦,在插入数据时有可能唯一主键冲、sql事务回滚、批量插入的时候,批量申请自增值等原因导致自增id是不连续的。
表定义的自增值达到上线后的逻辑是:再申请下一个id的时候,获取的是同一个值(最大值)。大家可以插入sql设置id是最大值,再insert一条不主动设置id的语句就可以验证这一结论啦。这个时候如果再插入就是报主键冲突咯~
这里提醒一下:-1()不是一个特别大的数,对于一个频繁插入删除数据的表来说,是可能会被用完的。因此在建表的时候你需要考察你的表是否有可能达到这个上限,如果有可能,就应该创建成8个字节的bigintunsigned。
InnoDB系统自增row_id
如果你创建的InnoDB表没有指定主键,那么InnoDB会给你创建一个不可见的,长度为6个字节的row_id。InnoDB维护了一个全局的dict_sys.row_id值,所有无主键的InnoDB表,每插入一行数据,都将当前的dict_sys.row_id值作为要插入数据的row_id,然后把dict_sys.row_id的值加1。
实际上,在代码实现时row_id是一个长度为8字节的无符号长整型(bigintunsigned)。但是,InnoDB在设计时,给row_id留的只是6个字节的长度,这样写到数据表中时只放了最后6个字节,所以row_id能写到数据表中的值,就有两个特征:
row_id写入表中的值范围,是从0到-1;
当dict_sys.row_id=2^48时,如果再有插入数据的行为要来申请row_id,拿到以后再取最后6个字节的话就是0。
虽然,2^48这个数字已经很大了,但是大家要知道一个系统是可以跑很久的,那么还是可能达到上限的,这时候再申请就会覆盖原来的记录了。因此,尽量不要选择这种方式!
Xid
MySQL中redolog和binlog相配合的时候,它们有一个共同的字段叫作Xid。它在MySQL中是用来对应事务的。
MySQL内部维护了一个全局变量global_query_id,每次执行语句的时候将它赋值给Query_id,然后给这个变量加1。如果当前语句是这个事务执行的第一条语句,那么MySQL还会同时把Query_id赋值给这个事务的Xid。而global_query_id是一个纯内存变量,重启之后就清零了。所以在同一个数据库实例中,不同事务的Xid也是有可能相同的。
Innodbtrx_id
InnoDB内部维护了一个max_trx_id全局变量,每次需要申请一个新的trx_id时,就获得max_trx_id的当前值,然后并将max_trx_id加1。
InnoDB数据可见性的核心思想是:每一行数据都记录了更新它的trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的trx_id做对比。但是这个过程有脏读存在,那么这个id就不会是原子性的,存在重复的可能性。
thread_id
其实,线程id才是MySQL中最常见的一种自增id。平时我们在查各种现场的时候,showprocesslist里面的第一列,就是thread_id。
thread_id的逻辑很好理解:系统保存了一个全局变量thread_id_counter,每新建一个连接,就将thread_id_counter赋值给这个新连接的线程变量。
thread_id_counter定义的大小是4个字节,因此达到-1后,它就会重置为0,然后继续增加。结果跟row_id一样,就会覆盖原有记录了。
上面介绍了几种MySQL自身的一些自增id,其实,实际运用中,我们也可能会选择外部的自增主键,然后持久化到数据库,以此来代替数据库自身的自增id。下面来说说吧。
Redis自增主键
其实外部自增主键的生成方式有很多,为什么我要介绍redis呢?因为我自己在实际应用中使用发现它的很多优点。
redis自身是原子性的,因此高并发也是线程安全的。假设主键字段长度20,我们以时间+自增数来构成主键,例如:8位日期+12自增数。那么,根据业务性质可以决定时间取年月日或者到毫秒级,那么在毫秒之间自增数的重复概率是极小极小的,基本的业务都能适用。
总结
上面介绍了好几种自增id,每种自增id有各自的应用场景,在达到上限后的表现也不同:
1、表的自增id达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误
2、row_id达到上限后,则会归0再重新递增,如果出现相同的row_id,后写的数据会覆盖之前的数据
3、Xid只需要不在同一个binlog文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计
4、InnoDB的max_trx_id递增值每次MySQL重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的bug,好在留给我们的时间还很充裕
5、thread_id是我们使用中最常见的,而且也是处理得最好的一个自增id逻辑了
6、redis外部自增,毫秒级别,理论上会出现重复值,但是概率极小,可以忽略不计
7、其实,每种自增id都有各自的适用场景,大家在平时使用中可以根据具体场景再选择。但是要未雨绸缪,因为系统的运行时间和数据的存储,这些都是要考虑在内的,综合考虑,选择一个在系统运行期间一定不会出现重复即刻。你学会了吗?