SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
1位:不使用,二进制中最高位为1的都是负数,ID一般都使用正整数,所以这个最高位固定是0。
41位:用来记录时间戳(毫秒)。可以表示2 ^ 41 - 1个数字,转化成单位年则是(2 ^ 41 - 1) / (1000 * 60 * 60 * 24 * 365) = 69年。
10位:用来记录工作机器id。可以部署在2 ^ 10 = 1024个节点,包括5位datacenterId和5位workerId。
5位可以表示的最大正整数是2 ^ 5 - 1 = 31,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId。
12位:序列号,用来记录同毫秒内产生的不同ID。可以用0、1、2、3、....4095这4096个数字,来表示同一机器同一时间截(毫秒)内产生的4096个ID序号
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。
public class IdWorker { private long workerId; private long datacenterId; private long sequence; public IdWorker(long workerId, long datacenterId, long sequence) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } log.info("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId); this.workerId = workerId; this.datacenterId = datacenterId; this.sequence = sequence; } private long twepoch = 1288834974657L; private long workerIdBits = 5L; private long datacenterIdBits = 5L; private long maxWorkerId = -1L ^ (-1L << workerIdBits); private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); private long sequenceBits = 12L; private long workerIdShift = sequenceBits; private long datacenterIdShift = sequenceBits + workerIdBits; private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private long sequenceMask = -1L ^ (-1L << sequenceBits); private long lastTimestamp = -1L; public long getWorkerId() { return workerId; } public long getDatacenterId() { return datacenterId; } public long getTimestamp() { return System.currentTimeMillis(); } public synchronized long nextId() { long timestamp = timeGen(); if (timestamp < lastTimestamp) { log.error("clock is moving backwards. Rejecting requests until %d.", lastTimestamp); throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0; } lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; } private long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); } //---------------测试--------------- public static void main(String[] args) { IdWorker worker = new IdWorker(1, 1, 1); for (int i = 0; i < 30; i++) { System.out.println(worker.nextId()); } } }
由于唯一ID生成服务肯定不会只有一个节点,所以只能保证唯一ID是局部递增的,无法保证全局递增。
从Snowflake算法的生成规则中可以看出,和时间戳强依赖,可能出现时钟回拨。
2020-12-12 12:12:12,生成分布式唯一ID。 2020-12-12 12:12:11,此时机器时钟回拨到前面的时间,就会出现ID重复的问题。
关闭时钟同步,避免产生时钟同步问题。这个不太现实,因为强依赖时间的系统,一般都得做时钟同步,避免时间严重错误。
记录上一次生成ID的时间,如果发现本次生成ID的时间戳小于上次的时间戳,说明时钟回拨了。
如果回拨时间在500ms以内,hang住请求,等待对应的回拨时间。等待时间结束之后,当前时间戳比上一次生成ID的时间戳要大了。
对于业务方而言,仅仅个别的时钟回拨情况之下,500ms,还在接受范围之内,请求一般情况下只要50ms。
如果时钟回拨时间在500ms - 5s,此时可以返回一个异常状态 + 异常持续时间给客户端,可以通知自行进行重试。
可以封装在唯一ID生成服务的客户端,客户端里封装自动重试机制,发现某台服务器返回的响应短时间内没法提供服务,自动就去请求其他机器上的服务获取唯一ID。
如果发现时钟回拨的太多,比如超过了1分钟,直接进行报警。
同时不再对外提供服务,从集群中下线。
要在内存里维护最近几秒内每一毫秒生成的ID值的最大值,时钟回拨一般是几十毫秒到几百毫秒,很少会超过秒的,所以保存最近几秒的就行了。
然后如果发生了时钟回拨,此时就看回拨到了哪一毫秒,因为时间戳是毫秒级的,接着就看那一毫秒。
从那一毫秒生产过的ID序号往后继续生成就可以了,后续每一毫秒都是依次类推,就可以完美避免重复问题,还不用等待。
将之前的数据持久化到磁盘中,启动的时候重新加载到内存中。
每次唯一ID生成服务启动会从ZK中获取最新的机器ID,删除自己之前的机器ID。