# distributeid **Repository Path**: simpleplus/distributeid ## Basic Information - **Project Name**: distributeid - **Description**: No description available - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2017-05-18 - **Last Updated**: 2020-12-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README #distributeid ### 概述 SnowFlake算法是Twitter设计的一个可以在分布式系统中生成唯一的ID的算法,它可以满足Twitter每秒上万条消息ID分配的请求,这些消息ID是唯一的且有大致的递增顺序。 ### 原理 SnowFlake算法产生的ID是一个64位的整型,结构如下(每一部分用“-”符号分隔): 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 1位标识部分,在java中由于long的最高位是符号位,正数是0,负数是1,一般生成的ID为正数,所以为0; 41位时间戳部分,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年; 10位节点部分,Twitter实现中使用前5位作为数据中心标识,后5位作为机器标识,可以部署1024个节点; 12位序列号部分,支持同一毫秒内同一个节点可以生成4096个ID; SnowFlake算法生成的ID大致上是按照时间递增的,用在分布式系统中时,需要注意数据中心标识和机器标识必须唯一,这样就能保证每个节点生成的ID都是唯一的。或许我们不一定都需要像上面那样使用5位作为数据中心标识,5位作为机器标识,可以根据我们业务的需要,灵活分配节点部分,如:若不需要数据中心,完全可以使用全部10位作为机器标识;若数据中心不多,也可以只使用3位作为数据中心,7位作为机器标识。 DistributedID的SDK服务器对外端口是16831,通过SDK方式接入时,需要了解SDK服务器的相关协议,编写客户端程序来跟SDK服务器通信。目前我自己写了一个DistributedID的SDK ——DistributedID-SDK,源码目前放在GitHub上托管,源码地址如下: 通信协议 HTTP服务器使用的是HTTP协议,如果需要了解HTTP协议的可以自行百度,在此处说的通信协议是对于DistributedID的SDK服务器来说的。一般来说,设计一个通信协议会有两个部分,请求的协议跟响应的协议,为了编码的方便,DistributedID在设计时把请求的协议跟响应的协议合并在一起使用,协议的格式如下: ``` | rqid | did | 4byte 8byte ``` rqid: 请求id,4个字节 ,客户端请求时生成,响应时返回; did: 分布式id,8个字节,服务器响应时生成,请求时传入0; 可能有的童鞋会有疑问,为什么要在协议里面增加一个rqid这个字段,除了麻烦,白白浪费带宽外,好像没什么用处。其实这个字段也是可以不要的,但是如果没有这个字段的话,在实现客户端SDK时,就不会使用异步请求的方式来跟服务器通信,因为客户端没办法标识响应的内容是哪一个请求。所以rqid是用来标识响应是属于哪一个请求的,rqid是客户端生成的一个Int类型的标识,在客户端中保持唯一。另外需要注意的是,客户端请求SDK服务器时,必须填写did这个字段,可以置为0,但不能不填。 实现 DistributedID生成分布式ID的核心是twitter snowflake算法,但是因为要提供http和sdk两种方式的接入,需要开启两个不同的通信线程对外服务。因为我希望获取分布式ID服务对于请求方式是透明的,即同一时刻无论哪一种方式获取到的分布式ID都是一样,所以设计时我把提供生成ID功能的SnowFlake包装成一个单实例,然后提供给HTTP服务线程和SDK服务线程使用。 由于协议的字节数是固定的,所以在解码时使用了Netty的FixedLengthFrameDecoder 解码器,固定包长为12个字节。编码时继承MessageToByteEncoder 编码器,编码逻辑也很简单,如下所示: ``` @Override protected void encode(ChannelHandlerContext channelHandlerContext, SdkProto sdkProto, ByteBuf out) throws Exception { out.writeInt(sdkProto.getRqid()); out.writeLong(sdkProto.getDid()); } ```` 为了保护服务器,DistributedID在设计时就考虑控制并发的处理数,防止并发请求过大时,服务器负载过重而崩溃。控制并发处理数的手段也不难,编码时使用了Java的Semapore信号量,初始化Semapore的数量为最大的并发数,每次调用SnowFlake算法生成分布式ID前,需要获取Semapore的许可,如果获取不到Semapore的许可,直接忽略此次请求,代码如下: ? ``` if (semaphore.tryAcquire(GlobalConfig.ACQUIRE_TIMEOUTMILLIS, TimeUnit.MILLISECONDS)) { try { sdkProto.setDid(snowFlake.nextId()); ctx.channel().writeAndFlush(sdkProto).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { semaphore.release(); } }); } catch (Exception e) { semaphore.release(); logger.error("SdkServerhandler error", e); } } else { sdkProto.setDid(-1); ctx.channel().writeAndFlush(sdkProto); String info = String.format("SdkServerHandler tryAcquire semaphore timeout, %dms, waiting thread " + "nums: %d availablePermit: %d", // GlobalConfig.ACQUIRE_TIMEOUTMILLIS, // this.semaphore.getQueueLength(), // this.semaphore.availablePermits() // ); logger.warn(info); throw new RemotingTooMuchRequestException(info); } ``` ### 部署 部署之前需要把项目源码打包成jar包,或者使用项目打包好的jar包,把jar包上传到服务器,执行如下命令: java -jar distributedid.jar 1 2 执行上面命令指定了两个参数1和2,前面的1代表数据中心标识,后面的2代表的是机器或进程标识. 如果不指定这两个参数,那么会使用默认的值1。如果只考虑部署单机服务器,那么可以不考虑这两个参数,如果需要分布式集群来生成ID时,需要指定数据中心标识ID和机器进程标识ID,并且每一个服务器的数据中心标识ID和机器进程标识ID作为联合键全局唯一,这样才能保证集群生成的ID都是唯一的。