在分布式系统中,有一些场景需要全局唯一的ID。在这种情况下,可以使用36位UUID来防止ID冲突。但是,UUID有一些缺点。首先,它们相对较长。此外,UUID通常是无序的。有时我们想使用更简单的ID,希望ID能够及时顺序生成。什么是雪花算法?Snowflake中文意思是雪花,所以常被称为雪花算法。算法TwitterSnowflake算法生成后是一个64位的long值,组成部分引入了时间戳,基本保持了自增SnowFlake算法的优点:高性能高可用:不依赖生成时的数据库,完全在内存中生成高位数据。吞吐量:每秒可生成百万个自增ID自增:存储在数据库中,索引效率高可能会导致ID冲突或重复的雪花算法形成如下图所示的雪花结构:包含四个未使用的组件:1bit,最高位为符号位,0表示正数,1表示负数,固定为0Timestamp:41bit,timeinmillisecondsStamp(41-bitlength可以用69年)identificationbit:5bitdatacenterID,5bitworkingmachineID,两个identificationbit的组合最多可以支持部署1024个节点Serialnumber:12bit增量序列号,表示节点在毫秒内重复生成,序列号是唯一的,12bit每毫秒可以生成4096个ID,序列号可以生成4096个唯一IDs在1毫秒内,因此4096*1000=409w个ID可以在1秒内生成。默认的雪花算法是64位,具体长度可以自己配置。如果想运行更长时间,增加时间戳的位数;如果需要支持更多的节点部署,增加标识位的长度;如果并发高,增加序号的位数场景定制雪花算法适用于场景因为雪花算法是有序自增的,保证了B+Tree索引结构插入的高性能MySQL。因此,在日常业务使用中,雪花算法更多的应用于数据库的主键ID和业务关联的主键雪花算法生成ID重复问题假设:一个订单微服务通过雪花算法生成ID,部署共三个节点,标识位一致。此时有200个并发,三个节点均匀分布。三个节点在相同的毫秒数和相同的序号下生成ID,会产生重复的ID。通过上面的假设场景,我们可以知道雪花算法产生ID冲突是有一定前提条件的。服务部署在集群中,部分机器标识位是一致的。业务有一定的并发量。没有并发量,就无法触发复制。问题ID生成时机:如何定义相同毫秒下序列号的一致标识位如果可以保证标识位不重复,那么SnowflakeID就不会重复通过上面的案例,我们知道了必要的条件用于ID重复。如果要避免服务中出现重复的ID,需要从标识位移开。我们先看看如何使用开源框架中的雪花算法来定义标识位。Mybatis-Plusv3.4.2雪花算法实现类Sequence提供了两种构造方法:不带参构造,自动生成dataCenterId和workerId;带参数构造,在创建Sequence时明确指定标识位Hutoolv5.7.9参考了Mybatis-PlusdataCenterId和workerId的生成方案,并提供了默认实现。如何生成dataCenterId和workerIdpublicstaticlonggetDataCenterId(longmaxDatacenterId){longid=1L;finalbyte[]mac=NetUtil.getLocalHardwareAddress();if(null!=mac){id=((0x000000FF&(long)mac[mac.length-2])|(0x0000FF00&(((long)mac[mac.length-1])<<8)))>>6;id=id%(maxDatacenterId+1);}returnid;}入参maxDatacenterId为固定值,代表数据中心ID的最大值,默认值为31,为什么最大值是31呢?因为5bit的最大二进制值是11111,刚好是31。一个不为空,可以通过Mac地址获取dataCenterId,dataCenterId的值与Mac地址有关。接下来看workerIdpublicstaticlonggetWorkerId(longdatacenterId,longmaxWorkerId){finalStringBuildermpid=newStringBuilder();mpid.append(datacenterId);try{mpid.append(RuntimeUtil.getPid());}catch(UtilExceptionigonre){//ignore}return(mpid.toString().hashCode()&0xffff)%(maxWorkerId+1);}入参maxWorkderId也是一个固定值,代表工作机器ID的最大值,默认值为31;datacenterId是从上面的getDatacenterId方法中取的name变量值是PID@IP,所以name需要按照@分割得到下标0,PID通过MAC+PID的hashcode得到16低位,进行运算最终得到workerId分配标识位。Mybatis-Plus标识位的获取依赖于Mac地址和进程PID,虽然可以尽量避免,但是还是有很小的几率如何定义标识位才不会重复呢?有两种解决方案:预分配和动态分配。申请标识位的方案不需要代码开发。当服务节点固定或项目较少时可以使用,但不能解决服务节点动态扩展的问题。动态分配在Redis、Zookeeper、MySQL等中间件中存储标识位,启动时请求标识位。在请求之后,标识位被更新为下一个可用的。通过存储标识位,出现一个问题:雪花算法的ID是在服务内唯一还是全局唯一。以Redis为例,如果想在服务内唯一,可以在自己的项目中使用存储标识位的Redis节点;如果是全局唯一的,那么所有使用雪花算法的应用都必须使用同一个Redis节点。两者的区别在于不同服务是否共享Redis。如果没有全局唯一的要求,最好让ID在服务内唯一,因为这样可以避免单点问题。如果服务中节点数超过1024个,需要额外扩容;可以扩展10位标识位,也可以选用开源的分布式ID框架动态分配实现方案Redis。存储一个Hash结构Key,其中包含两个键值对:dataCenterId和workerId。应用启动时,通过Lua脚本去Redis获取标识位。dataCenterId和workerId的获取和自增是在Lua脚本中完成的。调用返回后,可用标志可用。Lua脚本的具体逻辑如下:当获取第一个服务节点时,Redis可能没有snowflake_work_id_key的Hash,首先判断Hash是否存在,初始化Hash,dataCenterId和workerId初始化为0.如果Hash已经存在,则判断dataCenterId和workerId是否等于最大值31,如果满足则将dataCenterId和workerId初始化为0,返回dataCenterId和workerId共1024种排列组合。赋值时先赋值workerId,判断是否workerId!=31,满足则workerId自增返回;如果workerId=31,自增dataCenterId并将workerId设置为0dataCenterId,workerId一直被压低,形成一个环。通过Lua脚本的原子性,保证1024个节点下雪花算法的生成不会重复。如果标识位等于1024,则从头继续循环。开源的分布式ID框架Leaf和Uid都实现了雪花算法。Leaf另外提供数段方式生成美团LeafID:https://github.com/Meituan-Dianping/Leaf百度Uid:https://github.com/baidu/uid-generator雪花算法可以满足大部分场景。如非必要,不建议引入开源方案增加系统的复杂度。什么是雪花算法,如何解决雪花算法产生的ID冲突问题?关于SnowflakeAlgorithm产生的ID冲突的问题,在文章中给出了解决方案:assigningidentificationbits;通过分配雪花算法的组成标识位,实现默认的1024个节点ID生成下,可以查看Hutool或Mybatis-Plus雪花算法的具体实现,帮助大家更好的理解雪花算法不是万能的,不能适用于所有场景。如果要求ID全局唯一,服务节点超过1024个节点,可以选择修改算法本身的组成,即扩展标识位,或者选择开源方案:LEAF、UID
