数据库主键一定要自增吗?有哪些场景不建议自增?

数据库主键一定要自增吗?有哪些场景不建议自增?插图亿华云

我们平时建表的时候,一般会像下面这样。

CREATE TABLE `user` (

`id` int NOT NULL AUTO_INCREMENT COMMENT 主键,

`name` char(10) NOT NULL DEFAULT COMMENT 名字,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

出于习惯,我们一般会加一列id作为主键,而这个主键一般边上都有个AUTO_INCREMENT, 意思是这个主键是自增的。自增就是i ,也就是每次都加1。

但问题来了。

主键id不自增行不行?为什么要用自增id做主键?离谱点,没有主键可以吗?什么情况下不应该自增?被这么一波追问,念头都不通达了?

这篇文章,我会尝试回答这几个问题。

主键不自增行不行

当然是可以的。比如我们可以把建表sql里的AUTO_INCREMENT去掉。

CREATE TABLE `user` (

`id` int NOT NULL COMMENT 主键,

`name` char(10) NOT NULL DEFAULT COMMENT 名字,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

然后执行:

INSERT INTO `user` (`name`) VALUES (debug);

这时候会报错Field id doesnt have a default value。也就是说如果你不让主键自增的话,那你在写数据的时候需要自己指定id的值是多少,想要主键id是多少就写多少进去,不写就报错。

改成下面这样就好了。

INSERT INTO `user` (`id`,`name`) VALUES (10, debug);

为什么要用自增主键

我们在数据库里保存的数据就跟excel表一样,一行行似的。

数据库主键一定要自增吗?有哪些场景不建议自增?插图1亿华云

user表

而在底层,这一行行数据,就是保存在一个个16k大小的页里。

每次都去遍历所有的行性能会不好,于是为了加速搜索,我们可以根据主键id,从小到大排列这些行数据,将这些数据页用双向链表的形式组织起来,再将这些页里的部分信息提取出来放到一个新的16kb的数据页里,再加入层级的概念。于是,一个个数据页就被组织起来了,成为了一棵B 树索引。

数据库主键一定要自增吗?有哪些场景不建议自增?插图2亿华云

B 树结构

而当我们在建表sql里声明了PRIMARY KEY (id)时,mysql的innodb引擎,就会为主键id生成一个主键索引,里面就是通过B 树的形式来维护这套索引。

到这里,我们有两个点是需要关注的:

数据页大小是固定16k。数据页内,以及数据页之间,数据主键id都是从小到大排序的。

由于数据页大小固定了是16k,当我们需要插入一条新的数据,数据页会被慢慢放满,当超过16k时,这个数据页就有可能会进行分裂。

针对B 树叶子节点,如果主键是自增的,那它产生的id每次都比前一次要大,所以每次都会将数据加在B 树尾部,B 树的叶子节点本质上是双向链表,查找它的首部和尾部,时间复杂度O(1)。而如果此时最末尾的数据页满了,那创建个新的页就好。

数据库主键一定要自增吗?有哪些场景不建议自增?插图3亿华云

数据库主键一定要自增吗?有哪些场景不建议自增?插图4亿华云主键id自增的情况

如果主键不是自增的,比方说上次分配了id=7,这次分配了id=3,为了让新加入数据后B 树的叶子节点还能保持有序,它就需要往叶子结点的中间找,查找过程的时间复杂度是O(lgn),如果这个页正好也满了,这时候就需要进行页分裂了。并且页分裂操作本身是需要加悲观锁的。总体看下来,自增的主键遇到页分裂的可能性更少,因此性能也会更高。

数据库主键一定要自增吗?有哪些场景不建议自增?插图5亿华云

数据库主键一定要自增吗?有哪些场景不建议自增?插图6亿华云主键id不自增的情况

没有主键可以吗

mysql表如果没有主键索引,查个数据都得全表扫描,那既然它这么重要,我今天就不当人了,不声明主键,可以吗?

嗯,你完全可以不声明主键。

你确实可以在建表sql里写成这样。

CREATE TABLE `user` (

`name` char(10) NOT NULL DEFAULT COMMENT 名字

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

看起来确实是没有主键的样子。然而实际上,mysql的innodb引擎内部会帮你生成一个名为ROW_ID列,它是个6字节的隐藏列,你平时也看不到它,但实际上,它也是自增的。有了这层兜底机制保证,数据表肯定会有主键和主键索引。

跟ROW_ID被隐藏的列还有trx_id字段,用于记录当前这一行数据行是被哪个事务修改的,和一个roll_pointer字段,这个字段是用来指向当前这个数据行的上一个版本,通过这个字段,可以为这行数据形成一条版本链,从而实现多版本并发控制(MVCC)。有没有很眼熟,这个在之前写的文章里出现过。

数据库主键一定要自增吗?有哪些场景不建议自增?插图7亿华云

隐藏的row_id列

有没有建议主键不自增的场景

前面提到了主键自增可以带来很多好处,事实上大部分场景下,我们都建议主键设为自增。

那有没有不建议主键自增的场景呢?

mysql分库分表下的id

聊到分库分表,那我就需要说明下,递增和自增的区别了,自增就是每次都 1,而递增则是新的id比上一个id要大就行了,具体大多少,没关系。

之前写过一篇文章提到过,mysql在水平分库分表时,一般有两种方式。

一种分表方式是通过对id取模进行分表,这种要求递增就好,不要求严格自增,因为取模后数据会被分散到多个分表中,就算id是严格自增的,在分散之后,都只能保证每个分表里id只能是递增的。

数据库主键一定要自增吗?有哪些场景不建议自增?插图8亿华云

根据id取模分表

另一种分表方式是根据id的范围进行分表(分片),它会划出一定的范围,比如以2kw为一个分表的大小,那0~2kw就放在这张分表中,2kw~4kw放在另一张分表中,数据不断增加,分表也可以不断增加,非常适合动态扩容,但它要求id自增,如果id递增,数据则会出现大量空洞。举个例子,比如第一次分配id=2,第二次分配id=2kw,这时候第一张表的范围就被打满了,后面再分配一个id,比如是3kw,就只能存到2kw~4kw(第二张)的分表中。那我在0~2kw这个范围的分表,也就存了两条数据,这太浪费了。

数据库主键一定要自增吗?有哪些场景不建议自增?插图9亿华云

根据id范围分表

但不管哪种分表方式,一般是不可能继续用原来表里的自增主键的,原因也比较好理解,原来的每个表如果都从0开始自增的话,那好几个表就会出现好几次重复的id,根据id唯一的原则,这显然不合理。

所以我们在分库分表的场景下,插入的id都是专门的id服务生成的,如果是要严格自增的话,那一般会通过redis来获得,当然不会是一个id请求获取一次,一般会按批次去获得,比如一次性获得100个。快用完了再去获取下一批100个。

但这个方案有个问题,它严重依赖redis,如果redis挂了,那整个功能就傻了。

有没有不依赖于其他第三方组件的方法呢?

雪花算法

有,比如Twitter开源的雪花算法。

雪花算法通过64位有特殊含义的数字来组成id。

数据库主键一定要自增吗?有哪些场景不建议自增?插图10亿华云

数据库主键一定要自增吗?有哪些场景不建议自增?插图11亿华云雪花算法

首先第0位不用。

接下来的41位是时间戳。精度是毫秒,这个大小大概能表示个69年左右,因为时间戳随着时间流逝肯定是越来越大的,所以这部分决定了生成的id肯定是越来越大的。

再接下来的10位是指产生这些雪花算法的工作机器id,这样就可以让每个机器产生的id都具有相应的标识。

再接下来的12位,序列号,就是指这个工作机器里生成的递增数字。

可以看出,只要处于同一毫秒内,所有的雪花算法id的前42位的值都是一样的,因此在这一毫秒内,能产生的id数量就是

THE END
Copyright © 2024 亿华云