咱们平时建表的时候,个别会像上面这样。
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作为主键的话,内部通过抓包,很容易能够晓得新进用户量,成单量这些信息,所以须要审慎思考是否持续应用自增主键。