面试官:为什么《阿里巴巴Java开发手册》中强制要求主键id是自增的?

2,780 阅读7分钟

扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,即可关注微信公众号,阅读更多Spring源码分析Java并发编程Netty源码系列MySQL工作原理文章。

微信公众号

前言

在《阿里巴巴 Java 开发手册》第五章 MySQL 规定第九条中,强制规定了单表的主键 id 必须为无符号的 bigint 类型,且是自增的。为什么会这样强制规定呢?

阿里巴巴Java手册

为什么

通常主键 id 的数据类型有两种选择:字符串或者整数,主键通常要求是唯一的,如果使用字符串类型,我们可以选择 UUID 或者具有业务含义的字符串来作为主键。

对于 UUID 而言,它由 32 个字符+4 个'-'组成,长度为 36,虽然 UUID 能保证唯一性,但是它有两个致命的缺点:

  1. 不是递增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
  2. 太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。

对于整数的数字类型,MySQL 中主要有 int 和 bigint 类型。其中 int 占用 4 个字节,bigint 占用 8 个字节,这和 Java 中的 int 和 long 对应。如果使用无符号的 int 类型作为主键,那么主键的最大值为 2^32-1,即 4294967295,这个值不到 43 亿,似乎有点太小了。虽然一张表的数据,我们不可能让其达到 43 亿条(太大会影响性能),但是对于频繁进行插入、删除的表来说,43 亿这个值是可以达到的。而如果使用无符号的 bigint 类型的话,主键的最大值可以达到 2^64-1,这个数足够大了,如果以每秒插入 100 万条数据计算的,58 万年以后才能达到最大值。所以 bigint 作为主键的数据类型,完全不用担心超过最大值的问题。

而强制要求主键 id 是自增的,则是为了在数据插入的过程中,尽可能的避免索引树上页分裂的问题。

自增 id 用完后会出现什么现象

虽然前面已经解释了 bigint 作为主键已经足够我们使用了,但是我们不妨再考虑一下,如果真的碰到主键 id 被用完时,MySQL 会出现什么想象。

1. 指定主键

下面我们以 int 类型作为主键来举例说明一下,为了方便演示,我创建了一个示例表,由于 int 类型能表达的值最大为 4294967295,所以我让表的主键的初始自增值为 4294967295。 建表语句如下:

CREATE TABLE `test` (
`id` int ( 11 ) unsigned  AUTO_INCREMENT COMMENT '主键id',
`name` varchar ( 20 ) NOT NULL COMMENT '姓名',
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 4294967295 COMMENT '测试表';

# 然后向表中插入一条数据
insert into test(name) values("小明"); ### 插入成功

当我们再向表中插入一条数据时:

insert into test(name) values("小红");

发现插入失败,显示的错误信息如下图所示:

主键冲突

这是因为我们插入第一条数据的时候,获取到的主键 id 为 4294967295,然后 MySQL 尝试将这个自增值加 1 的时候,发现它已经达到了 int 能表达的最大值了,因此就保持不变。当我们再插入第二条数据的时候,获取的自增值还是 4294967295,所以就出现的主键冲突的错误。

2. rowid 作为主键

上面的例子是我们在表中明确指定了将哪一列作为主键列,而如果我们没有指定主键,那么 MySQL 的 InnoDB 引擎就会使用系统 rowid 作为表的主键。这个 rowid 是所有表共享的一个变量,所有没有主键的表都会使用这个 rowid 作为主键。每插入一行数据,这个全局的 rowid 的值就会加 1。

这个 rowid 实际上使用的是无符号的 bigint 类型,因此会占用 8 个字节,但是 InnoDB 只会用后面的 6 个字节来表示主键,前面的两个字节并不用来表示主键,因此 rowid 能表示主键的最大值为 2^48-1。如果达到最大值后,也就是后面的 48bit 的值全是 1 后,再尝试进行+1 操作时,就会向高位进位,后面的 48 bit 就变成了 0,所以此时表示的主键的值就变成了 0,后面再一次自增,就又从 1 开始了。因此如果使用 rowid 作为主键的话,当达到最大值后,会出现数据覆盖。

为什么说会出现数据覆盖呢?我们可以通过下面这个例子来说明。

假设现在有一行数据,它隐藏的主键 id(rowid)值为 1,name 列的值为'小明'。当某一天 rowid 达到 2^48-1 后,再次自增时,rowid 表示的主键值就从 0 开始了,当我们再插入一条 name 等于'小红'的数据是,rowid 自增得到的值也表示的是 1,这样新插入的数据就变成了(1,小红),覆盖了前面(1,小明)这条数据。

自增 id 用完了怎么办

虽然 bigint 足够我们使用了,但如果真的出现了极端情况,bigint 也用完了(有些奇葩面试官可能就是这么刚),那应该怎么办呢?

没辙了,只能选择字符串作为主键了。实际上如果真的出现了这种情况,我们应该思考的是我们的表设计的有问题了,在出现这么大数据量之前,我们就应该提前想到分库分表了。

总结

本文从《阿里巴巴 Java 开发手册》中的一条强制规定出发,分析了为什么要求单表的主键必须是无符号 bigint 类型的自增主键,一是因为 bigint 能表示的数足够大,二是自增能使主键 id 有序,在插入时能尽量避免索引树的页分裂。

然后又举例说明了自增主键 id 用完了,会出现什么样的现象。如果表指定了主键,当主键自增值用完后,再插入数据就会出现主键冲突的异常;如果表没有指定主键,InnoDB 则会使用全局的 rowid 作为主键,当 rowid 的主键自增值达到上限后,就会从 1 开始循环,最终覆盖数据。对比这两者,我们应该更倾向于前者,因为它至少能让开发人员知道系统出了异常,而后者则直接在开发人员不知道的情况下覆盖了数据,这是不可取的。

其他

微信公众号