关于后端:数据库主键一定要自增的吗有哪些场景下不建议自增

27次阅读

共计 5233 个字符,预计需要花费 14 分钟才能阅读完成。

咱们平时建表的时候,个别会像上面这样。
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’ doesn’t have a default value。也就是说如果你不让主键自增的话,那你在写数据的时候须要本人指定 id 的值是多少,想要主键 id 是多少就写多少进去,不写就报错。
改成上面这样就好了
INSERT INTO user (id,name) VALUES (10, ‘debug’);
复制代码

为什么要用自增主键
咱们在数据库里保留的数据就跟 excel 表一样,一行行似的。

而在底层,这一行行数据,就是保留在一个个 16k 大小的页里。
每次都去遍历所有的行性能会不好,于是为了减速搜寻,咱们能够依据主键 id,从小到大排列这些行数据,将这些数据页用双向链表的模式组织起来,再将这些页里的局部信息提取进去放到一个新的 16kb 的数据页里,再退出层级的概念。于是,一个个数据页就被组织起来了,成为了一棵 B + 树索引。

而当咱们在建表 sql 里申明了 PRIMARY KEY (id)时,mysql 的 innodb 引擎,就会为主键 id 生成一个主键索引,外面就是通过 B + 树的模式来保护这套索引。
到这里,咱们有两个点是须要关注的:

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

因为数据页大小固定了是 16k,当咱们须要插入一条新的数据,数据页会被缓缓放满,当超过 16k 时,这个数据页就有可能会进行决裂。
针对 B + 树叶子节点,如果主键是自增的,那它产生的 id 每次都比前一次要大,所以每次都会将数据加在 B + 树尾部,B+ 树的叶子节点实质上是双向链表,查找它的首部和尾部,工夫复杂度 O(1)。而如果此时最开端的数据页满了,那创立个新的页就好。

如果主键不是自增的,比方说上次调配了 id=7,这次调配了 id=3,为了让新退出数据后 B + 树的叶子节点还能放弃有序,它就须要往叶子结点的两头找,查找过程的工夫复杂度是 O(lgn),如果这个页正好也满了,这时候就须要进行页决裂了。并且页决裂操作自身是须要加乐观锁的。总体看下来,自增的主键遇到页决裂的可能性更少,因而性能也会更高。

没有主键能够吗
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)。有没有很眼生,这个在之前写的文章里呈现过。

有没有倡议主键不自增的场景
后面提到了主键自增能够带来很多益处,事实上大部分场景下,咱们都倡议主键设为自增。
那有没有不倡议主键自增的场景呢?

mysql 分库分表下的 id
聊到分库分表,那我就须要阐明下,递增和自增的区别了,自增就是每次都 +1,而递增则是新的 id 比上一个 id 要大就行了,具体大多少,没关系。
之前写过一篇文章提到过,mysql 在程度分库分表时,个别有两种形式。
一种分表形式是通过对 id 取模进行分表,这种要求递增就好,不要求严格自增,因为取模后数据会被扩散到多个分表中,就算 id 是严格自增的,在扩散之后,都只能保障每个分表里 id 只能是递增的。

另一种分表形式是依据 id 的范畴进行分表(分片),它会划出肯定的范畴,比方以 2kw 为一个分表的大小,那 02kw 就放在这张分表中,2kw4kw 放在另一张分表中,数据一直减少,分表也能够一直减少,非常适合动静扩容,但它要求 id 自增,如果 id 递增,数据则会呈现大量空洞。举个例子,比方第一次调配 id=2,第二次调配 id=2kw,这时候第一张表的范畴就被打满了,前面再调配一个 id,比方是 3kw,就只能存到 2kw4kw(第二张)的分表中。那我在 02kw 这个范畴的分表,也就存了两条数据,这太节约了。

但不论哪种分表形式,个别是不可能持续用原来表里的自增主键的,起因也比拟好了解,原来的每个表如果都从 0 开始自增的话,那好几个表就会呈现好几次反复的 id,依据 id 惟一的准则,这显然不合理。

所以咱们在分库分表的场景下,插入的 id 都是专门的 id 服务生成的,如果是要严格自增的话,那个别会通过 redis 来取得,当然不会是一个 id 申请获取一次,个别会按批次去取得,比方一次性取得 100 个。快用完了再去获取下一批 100 个。
但这个计划有个问题,它重大依赖 redis,如果 redis 挂了,那整个性能就傻了。
有没有不依赖于其余第三方组件的办法呢?

雪花算法
有,比方 Twitter 开源的雪花算法。
雪花算法通过 64 位有非凡含意的数字来组成 id。

首先第 0 位不必。
接下来的 41 位是工夫戳。精度是毫秒,这个大小大略能示意个 69 年左右,因为工夫戳随着工夫流逝必定是越来越大的,所以这部分决定了生成的 id 必定是越来越大的。
再接下来的 10 位是指产生这些雪花算法的工作机器 id,这样就能够让每个机器产生的 id 都具备相应的标识。
再接下来的 12 位,序列号,就是指这个工作机器里生成的递增数字。
能够看出,只有处于同一毫秒内,所有的雪花算法 id 的前 42 位的值都是一样的,因而在这一毫秒内,能产生的 id 数量就是 2 的 10 次方✖️2 的 12 次方,大略 400w,必定是够用了,甚至有点多了。

然而!
仔细的兄弟们必定也发现了,雪花算法它算出的数字动不动就比上次的数字多个几百几万的,也就是它生成的 id 是趋势递增的,并不是严格 + 1 自增 的,也就是说它并不太适宜于依据范畴来分表的场景。这是个十分疼的问题。
还有个小问题是,那 10 位工作机器 id,我每次扩容一个工作机器,这个机器怎么晓得本人的 id 是多少呢?是不是得从某个中央读过来。
那有没有一种生成 id 生成计划,既能让分库分表能做到很好的反对动静扩容,又能像雪花算法那样并不依赖 redis 这样的第三方服务。
有。这就是这篇文章的重点了。

适宜分库分表的 uuid 算法
咱们能够参考雪花算法的实现,设计成上面这样。留神上面的每一位,都是十进制,而不是二进制。

结尾的 12 位仍然是工夫,但并不是工夫戳,雪花算法的工夫戳准确到毫秒,咱们用不上这么细,咱们改为 yyMMddHHmmss,留神结尾的 yy 是两位,也就是这个计划能保障到 2099 年之前,id 都不会反复,能用到反复,那也是真·百年企业。同样因为最后面是工夫,随着工夫流逝,也能保障 id 趋势递增。
接下来的 10 位,用十进制的形式示意工作机器的 ip,就能够把 12 位的 ip 转为 10 位的数字,它能够保障全局惟一,只有服务起来了,也就晓得本人的 ip 是多少了,不须要像雪花算法那样从别的中央去读取 worker id 了,又是一个小细节。

在接下来的 6 位,就用于生成序列号,它能反对每秒钟生成 100w 个 id。
最初的 4 位,也是这个 id 算法最妙的局部。它前 2 位代表分库 id,后 2 位代表分表 id。也就是反对一共 100*100=1w 张分表。

举个例子,假如我只用了 1 个分库,当我一开始只有 3 张分表的状况下,那我能够通过配置,要求生成的 uuid 最初面的 2 位,取值只能是 [0,1,2],别离对应三个表。这样我生成进去的 id,就能十分平均的落到三个分表中,这还顺带解决了单个分表热点写入的问题。
如果随着业务一直倒退,须要新退出两张新的表 (3 和 4),同时第 0 张表有点满了,不心愿再被写了,那就将配置改为[1,2,3,4],这样生成的 id 就不会再插入到对应的 0 表中。同时还能够退出生成 id 的概率和权重来调整哪个分表落更多数据。
有了这个新的 uuid 计划,咱们既能够保障生成的数据趋势递增,同时也能十分不便扩大分表。十分 nice。

数据库有那么多种,mysql 只是其中一种,那其余数据库也是要求主键自增吗?

tidb 的主键 id 不倡议自增
tidb 是一款分布式数据库,作为 mysql 分库分表场景下的代替产品,能够更好的对数据进行分片。
它通过引入 Range 的概念进行数据表分片,比方第一个分片表的 id 在 02kw,第二个分片表的 id 在 2kw4kw。这其实就是依据 id 范畴进行数据库分表。
它的语法简直跟 mysql 统一,用起来大部分时候是无感的。
但跟 mysql 有一点很不一样的就是,mysql 倡议 id 自增,但 tidb 却倡议应用随机的 uuid。起因是如果 id 自增的话,依据范畴分片的规定,一段时间内生成的 id 简直都会落到同一个分片上,比方下图,从 3kw 开始的自增 uuid,简直都落到 range 1 这个分片中,而其余表却简直不会有写入,性能没有被利用起来。呈现一表有难,多表围观的局面,这种状况又叫写热点问题。

所以为了充沛的利用多个分表的写入能力,tidb 倡议咱们写入时应用随机 id,这样数据就能被平均扩散到多个分片中。

用户 id 不倡议用自增 id
后面提到的不倡议应用自增 id 的场景,都是技术起因导致的,而上面介绍的这个,单纯是因为业务。
举个例子吧。
如果你能晓得一个产品每个月,新增的用户数有多少,这个对你来说会是有用的信息吗?
对程序员来说,可能这个信息价值不大。
但如果你是做投资的呢,或者是剖析竞争对手呢?
那反过来。
如果你发现你的竞争对手,总能十分清晰的晓得你的产品每个月新进的注册用户是多少人,你会不会心里毛毛的?
如果真呈现了这问题,先不要想是不是有内鬼,先查看下你的用户表主键是不是自增的。

如果用户 id 是自增的,那他人只有每个月都注册一个新用户,而后抓包失去这个用户的 user_id,而后跟上个月的值减一下,就晓得这个月新进多少用户了。
同样的场景有很多,有时候你去小店吃饭,发票上就写了你是明天的第几单,那大略就能预计明天店家做了多少单。你是店家,你心里也不难受吧。
再比如说一些小 app 的商品订单 id,如果也做成自增的,那就很容易能够晓得这个月成了多少单。
相似的事件有很多,这些场景都倡议应用趋势递增的 uuid 作为主键。
当然,主键放弃自增,然而不裸露给前端,那也行,那后面的话,你当我没说过。

总结

建表 sql 里主键边上的 AUTO_INCREMENT,能够让主键自增,去掉它是能够的,但这就须要你在 insert 的时候本人设置主键的值。
建表 sql 里的 PRIMARY KEY 是用来申明主键的,如果去掉,那也能建表胜利,但 mysql 外部会给你偷偷建一个 ROW_ID 的暗藏列作为主键。
因为 mysql 应用 B + 树索引,叶子节点是从小到大排序的,如果应用自增 id 做主键,这样每次数据都加在 B + 树的最初,比起每次加在 B + 树两头的形式,加在最初能够无效缩小页决裂的问题。
在分库分表的场景下,咱们能够通过 redis 等第三方组件来取得严格自增的主键 id。如果不想依赖 redis,能够参考雪花算法进行魔改,既能保证数据趋势递增,也能很好的满足分库分表的动静扩容。
并不是所有数据库都倡议应用自增 id 作为主键,比方 tidb 就举荐应用随机 id,这样能够无效防止写热点的问题。而对于一些敏感数据,比方用户 id,订单 id 等,如果应用自增 id 作为主键的话,内部通过抓包,很容易能够晓得新进用户量,成单量这些信息,所以须要审慎思考是否持续应用自增主键。

正文完
 0