当数据规模达到一定量级的时候会影响到数据库的性能,那么这个时候我们一般会沿着AKF的Z轴使用分库分表的策略,以降低单表的数据量,从而提高数据库的性能。但是分库分表后,我们怎么保证ID的全局唯一性呢?这个时候ID生成器就登场了。
ID应该遵循的原则
1、ID应该是按时间有序的,因为在某些场景上可能会用到,比如获取商品的评论,一般需要按照评论的时间倒序显示,如果评论ID是无序的那边就需要添加额外的字段排序。另外ID如果是有序,可以提升数据库的性能,因为有序的ID,对于关系型数据库来说可以有效的实现插入数据的顺序写磁盘,如果ID是无序的,那么每次写入的位置都不一样是随机写,更严重的是可能需要移动数据与页分裂,从而导致页空洞,不能高效的利用磁盘空间;
2、从ID中应该能够反解出ID所属的业务,这样在排查问题的过程中可能有帮助;
3、ID占用内存应该足够小,最好能够用一个64bit的整数表示,从而能够提升数据库的性能,节约内存与磁盘空间。
基于如上的原因,UUID虽然很高效且唯一,但显然是不适合作为全局ID的,因为其不是有序的,没有业务含义,同时还需要占用比较多的空间。
为此我们一般用snowflake算法(雪花算法)或者通过数据库生成ID做为全局ID。
snowflake算法(雪花算法)
snowflake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0;
41位,用来记录时间戳(毫秒)。41位可以表示2^41个数字,也就是说41位可以表示2^41个毫秒的值,转化成单位年则是2^41/(1000*60*60*24*365)约等于69年;
10位,用来记录工作机器id。可以部署在2^10=1024个节点,包括5位IDC Id和5位workerId,可以表示的最大正整数是2^5−1=31;
12位,序列号,用来记录同毫秒内产生的不同id。 12位(bit)可以表示的最大正整数是2^12−1=4095,即同一机器同一时间截(毫秒)内产生的4096个ID序号。
在实际使用过程中,我们一般会做如下的调整:
1、适当调整不同位数的含义:如时间戳字段可以以10ms为单位,可以用40bit;工作机器id可以做一些拆分把业务id也放进去或者IDC id与workerId的位数做一些调整;序列号的位数也可以适当做一些调整;
2、如果系统的规模不大,ID生成可以内嵌到业务代码中,这样ID生成就会非常的高效,也不会对系统的性能产生影响,但是系统主机的个数一般还是比较多的,这时就得引入第三方组件来管理主机id;
3、如果系统的规模比较大,我们可以在一个机房中部署2个或更多个ID生成服务,每个机房尽量调用本机房的ID生成服务。这样每次获取ID多增加的时间成本也很有限,如果还想省却这个时间,业务可以批量获取,不过一般不建议批量获取。
不过 snowflake算法也有如下的问题:
1、我们一般需要打开时钟同步功能,这样ID才能够最大化的保证按照时间有序,但是时钟同步打开后,就可能会时钟回拨了,如果时钟回拨了,那么生成的ID就会重复,为此我们一般打开时钟同步的同时关闭时钟回拨功能;
2、序列号的位数有限,能表示的ID个数有限,时钟同步的时候,如果某台服务器快了很多,虽然关闭了时钟回拨,但是在时间追赶上前,ID可能已经用完,当自增序列号用完了,我们可以做如下的工作:停止ID生成服务并告警、如果时钟回拨小于一定的阈值则等待、如大于一定的阈值则通过第三方组件如ZK重新生成一个workerid或者自增时间戳借用下一个时间戳的ID;
3、服务重启后,ID可能会重复,为此我们一般需要定期保存时间戳,重启后的时间戳必须大于保存的时间戳+几倍保存间隔时间(如3倍),为什么要几倍呢,主要是考虑到数据丢失的情况,但是如果保存到本地硬盘且每次保存都fsync,此时1倍即可。重启后如果小于可以像第二点那样类似处理;
4、如果请求ID的QPS不高,比如每毫秒一个,那么每次获取的ID的尾号都是0,那么基于ID做分库分表,可能数据分布就会不均,此时我们可以增加时间戳的时间间隔或者序列号每次从随机一个值开始自增。
更多方案介绍参见:
https://tech.meituan.com/2017/04/21/mt-leaf.html