写点什么

分布式 ID 生成策略,我和面试官掰扯了一个小时

用户头像
极客good
关注
发布于: 刚刚

我能有什么办法阿,完全没办法,只能从裤兜里拿出笔和纸,快速的画了一张图。



我:我这里假设有三个数据库,为每一个数据库设置初始值,设置初始值可以通过下面的 sql 进行设置:


set?@@auto_increment_offset?=?1;?????//?设置初始值


set?@@auto_increment_increment?=?2;??//?设置步长


我:三个数据的初始值分别设置为 1、2、3,一般步长设置为数据库的数据,这里数据库数量为 3,所以步长也设置为 3。


面试官:若是面对再次扩容的情况呢?


我:恩额,扩容的情况是这种方法的一个缺点,上面我说的步长一般设置为数据库的数量,这是在确保后期不会扩容的情况下,若是确定后期会有扩容情况,在前期设计的的时候可以将步长设置长一点,「预留一些初始值给后续扩容使用」。


我:总之,这种方案还是优缺点的,但是也有自己的优点,缺点就是:「后期可能会面对无 ID 初始值可分的窘境,数据库总归是数据库,抗高并发也是有限的」。


我:它的优点就是算是解决了「DB 单点的问题」。


面试官:恩额。

批量申请自增 ID

我:「批量申请自增 ID」的解决方案可以解决无 ID 可分的问题,它的原理就是一次性给对应的数据库上分配一批的 ID 值进行消费,使用完了,再回来申请。


这次我很自觉的从裤兜里拿出笔和纸,画出了下面的这张图,历史总是那么惊人的相似。



我:在设计的初始阶段可以设计一个有初始值字段,并有步长字段的表,当每次要申请批量 ID 的时候,就可以去该表中申请,每次申请后「初始值=上一次的初始值+步长」。


我:这样就能保持初始值是每一个申请的 ID 的最大值,避免了 ID 的重复,并且每次都会有 ID 使用,一次就会生成一批的 id 来使用,这样访问数据库的次数大大减少。


我:但是这一种方案依旧有自己的缺点,依然不能抗真正意义上的高并发。

UUID 生成

我:第四种方式是使用「UUID 生成」的方式生成分布式 ID,UUID 的核心思想是使用「机器的网卡、当地时间、一个随机数」来生成 UUID。


我:使用 UUID 的方式只需要调用 UUID.randomUUID().toString()就可以生成,这种方式方便简单,本地生成,不会消耗网络。


我:当时简单的东西,出现的问题就会越多,不利于存储,16 字节 128 位,通常是以 36 位长度的字符串表示,很多的场景都不适合。


我:并且 UUID 生成的无序的字符串,查询效率低下,没有实际的业务含义,不具备自增特性,所以都不会使用 UUID 作为分布式 ID 来使用。


面试官:恩额,那你知道生成 UUID 的方式有几种吗?不知道没关系,这个只是作为一个扩展。


我:这个我只知道可以通过「当前的时间戳及机器 mac 地址」来生成,可以确保生成的 UUID 全球唯一,其它的没有了解过。


面试官:嗯嗯,没关系的。

Redis 的方式

我:为了解决上面纯关系型数据库生成分布式 ID 无法抗高并发的问题,可以使用 Redis 的方式来生成分布式 ID。


我:Redis 本身有 incr 和 increby 这样自增的命令,保证原子性,生成的 ID 也是有序的。


我:Redis 基于内存操作,性能高效,不依赖于数据库,数据天然有序,利于分页和排序。


我:但是这个方案也会有自己的缺点,因为增加了中间件,需要自己编码实现工作量增大,增加复杂度。


我:使用 Redis 的方式还要考虑持久化,Redis 的持久化有两种「RDB 和 AOF」,「RDB 是以快照的形式进行持久化,会丢失上一次快照至此时间的数据」。


我:「AOF 可以设置一秒持久化一次,丢失的数据是秒内的」,也会存在可能上一次自增后的秒内的 ID 没有持久化的问题。


我:但是这种方法相对于上面的关系型数据库生成分布式 ID 的方法而言,已经优越了许多。


我:若是数据量比较大的话,重启 Redis 的时间也会比较长,可以采用 Redis 的集群方式。


面试官:你能手写一下 Redis 的生成分布式 ID 的工具类代码吗?


我奔溃了,我最怕手写了,因为工具类这种东西,基本就是项目开始的时候写一次,后面对后市重复使用,记不住,还要手写,这也太难为我怕虎了吧。


我:手写应该不行,因为有些 API 记不住,工具类基本就是项目开始的时候写一些,后续都没有去看过了,没有专门去记它。


我:我可以使用您的电脑吗?使用电脑应该可以敲出这些工具类。


面试官:可以的,这边电脑给你,你在这个测试项目下吧。


我:好的,谢谢。


时间流逝中........


大概敲了几分钟,废了九牛二虎之力,终于敲出来了,有好多 API 记不住,只能慢慢的找了,写了主要两种方式来生成分布式 ID。


第一种是使用 RedisAtomicLong 原子类使用 CAS 操作来生成 ID。


@Service


public?class?RedisSequenceFactory?{


@Autowired


RedisTemplate<String,?String>?redisTemplate;


public?void?setSeq(String?key,?int?value,?Date?expireTime)?{


RedisAtomicLong?counter?=?new?RedisAtomicLong(key,?redisTemplate.getConnectionFactory());


counter.set(value);


counter.expireAt(expireTime);


}


public?void?setSeq(String?key,?int?value,?long?timeout,?TimeUnit?unit)?{


RedisAtomicLong?counter?=?new?RedisAtomicLong(key,?redisTemplate.getConnectionFactory());


counter.set(value);


counter.expire(timeout,?unit);


}


public?long?generate(String?key)?{


RedisAtomicLong?counter?=?new?RedisAtomicLong(key,?redisTemplate.getConnectionFactory());


return?counter.incrementAndGet();


}


public?long?incr(String?key,?Date?expireTime)?{


RedisAtomicLong?counter?=?new?RedisAtomicLong(key,?redisTemplate.getConnectionFactory());


counter.expireAt(expireTime);


return?counter.incrementAndGet();


}


public?long?incr(String?key,?int?increment)?{


RedisAtomicLong?counter?=?new?RedisAtomicLong(key,?redisTemplate.getConnectionFactory());


return?counter.addAndGet(increment);


}


public?long?incr(String?key,?int?increment,?Date?expireTime)?{


RedisAtomicLong?counter?=?new?RedisAtomicLong(key,?redisTemplate.getConnectionFactory());


counter.expireAt(expireTime);


return?counter.addAndGet(increment);


}


}?


第二种是使用 redisTemplate.opsForHash()和结合 UUID 的方式来生成生成 ID。


public?Long?getSeq(String?key,String?hashKey,Long?delta)?throws?BusinessException{


try?{


if?(null?==?delta)?{


delta=1L;


}


return?redisTemplate.opsForHash().increment(key,?hashKey,?delta);


}?catch?(Exception?e)?{??//?若是 redis 宕机就采用 uuid 的方式


int?first?=?new?Random(10).nextInt(8)?+?1;


int?randNo=UUID.randomUUID().toString().hashCode();


if?(randNo?<?0)?{


randNo=-randNo;


}


return?Long.valueOf(first?+?String.format("d",?randNo));


}


}?


我把电脑移回给面试官,他很快的扫了一下我的代码,说了一句。


面试官:小伙子,不写注释哦,这个习惯不好哦。


我:哦哦,谢谢提醒,不好意思,下次我会注意的。

雪花算法

我:第六种方式是「雪花算法」,也是现在市面上比较流行的生成分布式 ID 的方法。


说着说着,我知道画图又是必不可少的了,于是在桌子上有画了起来,面试官好奇的看看我,知道了我在干啥,又耐心的等了等。


我:他是采用 64bit 作为 ID 生成类型,并且将 64bit 划分为,如下图的几段。


我顺手把我画的图递给他看了看,接着对着这个图进行解释。



我:第一位作为标识位,因为 Java 中 long 类型的时代符号的,因为 ID 位正数,所以第一位位 0。


我:接着的 41bit 是时间戳,毫秒级位单位,注意这里的时间戳并不是指当前时间的时间戳,而是值之间差(「当前时间-开始时间」)。


我:这里的开始时间一般是指 ID 生成器的开始时间,是由我们程序自己指定的。


我:接着后面的 10bit:包括 5 位的「数据中心标识 ID(datacenterId)和 5 位的机器标识 ID(workerId)」,可以最多标识 1024 个节点(1<<10=1024)。


我:最的 12 位是序列号,12 位的计数顺序支持每个节点每毫秒差生 4096 序列号(1<<12=4096)。


我:雪花算法使用数据中心 ID 和机器 ID 作为标识,不会产生 ID 的重复,并且是在本地生成,不会


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


消耗网络,效率高,有数据显示,每秒能生成 26 万个 ID。


我:但是雪花算法也是又自己的缺点,因为雪花算法的计算依赖于时间,若是系统时间回拨,就会产生重复 ID 的情况。


面试官:那对于时间回拨产生重复 ID 的情况,你有什么比较好的解决方案吗?


我:在雪花算法的实现中,若是其前置的时间等于当前的时间,就抛出异常,也可以关闭掉时间回拨。


我:对于回拨时间比较短的,可以等待回拨时间过后再生成 ID。


面试官:你可以帮我敲一个雪花算法吗?我这键盘给你。


我:……


我:好的。


时间流逝中......


过了几分钟时间,也总算是把雪花算法给敲出来了,真正要老命,面个试怎么就那么难呢?


/**


*?雪花算法


*?@author:黎杜


*/


public?class?SnowflakeIdWorker?{


/**?开始时间截?*/


private?final?long?twepoch?=?1530051700000L;


/**?机器 id 的位数?*/


private?final?long?workerIdBits?=?5L;


/**?数据标识 id 的位数?*/


private?final?long?datacenterIdBits?=?5L;


/**?最大的机器 id,结果是 31?*/


private?final?long?maxWorkerId?=?-1L?^?(-1L?<<?workerIdBits);


/**?最大的数据标识 id,结果是 31?*/


private?final?long?maxDatacenterId?=?-1L?^?(-1L?<<?datacenterIdBits);


/**?序列的位数?*/


private?final?long?sequenceBits?=?12L;


/**?机器 ID 向左移 12 位?*/


private?final?long?workerIdShift?=?sequenceBits;

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
分布式ID生成策略,我和面试官掰扯了一个小时