分布式SnowFlake算法
hirikisnowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。
其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。
具体介绍
雪花算法是 64 位 的二进制,一共包含了四部分:
- 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
- 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。
- 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。
- 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
| package cn.ray.domain.support.ids;
import org.slf4j.Logger; import org.slf4j.LoggerFactory;
public class SnowFlake {
private Logger logger = LoggerFactory.getLogger(SnowFlake.class);
private long dataCenterId;
private long workerId;
private long sequence;
private final long twepoch = 1589923200000L;
private final long dataCenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private final long workerIdShift = sequenceBits;
private final long dataCenterIdShift = workerIdBits + sequenceBits;
private final long timestampLeftShift = dataCenterIdBits + workerIdBits + sequenceBits;
private long lastTimestamp = -1L;
public SnowFlake(long dataCenterId, long workerId) { this(dataCenterId,workerId,0); }
public SnowFlake(long dataCenterId, long workerId, long sequence) { if ( dataCenterId>maxDataCenterId || dataCenterId < 0) { throw new IllegalArgumentException(String.format("机房ID 大于最大值 %d 或者 小于 0",maxDataCenterId)); } if ( workerId>maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("机器ID 大于最大值 %d 或者 小于 0",maxWorkerId)); } logger.info("机器启动中: 时间戳偏移量:{},机房ID占位:{},机器ID占位:{},序列号占位:{},机器ID:{}", timestampLeftShift,dataCenterIdBits,workerIdBits,sequenceBits,workerId); this.dataCenterId = dataCenterId; this.workerId = workerId; this.sequence = sequence; }
public long getDataCenterId() { return dataCenterId; }
public long getWorkerId() { return workerId; }
public long getLastTimestamp() { return lastTimestamp; }
public synchronized long nextId(){ long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) { logger.error("时间回退,最新时间戳:{}",lastTimestamp); throw new RuntimeException(String.format("时间回退,拒绝生成ID,实际相差: %d",lastTimestamp - currentTimestamp)); }
if (currentTimestamp == lastTimestamp) { sequence = (sequenceMask+1) & sequenceMask;
if(sequence == 0) { currentTimestamp = nextTimestamp(lastTimestamp); } } else { sequence = 0; }
lastTimestamp = currentTimestamp;
return ((currentTimestamp - twepoch) << timestampLeftShift) | (dataCenterId << dataCenterIdShift) | (workerId << workerIdShift) | sequence; }
private long nextTimestamp(long lastTimestamp) { long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp <= lastTimestamp) { currentTimestamp = System.currentTimeMillis(); }
return currentTimestamp; } }
|
- 最后将三部分(时间戳、机器ID、序列号)按照偏移量进行移位操作,然后按位或将其组合即可得到随机生成的唯一ID
优化
由于41位是时间戳,我们的时间计算是从1970年开始的,只能使用69年,为了不浪费,其实我们可以用时间的相对值,也就是以项目开始的时间为基准时间,往后可以使用69年。获取唯一ID的服务,对处理速度要求比较高,所以我们全部使用位运算以及位移操作,获取当前时间可以使用System.currentTimeMillis()
。
时间回拨问题
在获取时间的时候,可能会出现时间回拨
的问题,时间回拨其实就是服务器上的时间突然倒退到之前的时间。
- 人为原因,把系统环境的时间改了。
- 有时候不同的机器上需要同步时间,可能不同机器之间存在误差,那么可能会出现时间回拨问题。
解决方案
- 回拨时间小的时候,不生成 ID,循环等待到时间点到达。
- 上面的方案只适合时钟回拨较小的,如果间隔过大,阻塞等待,肯定是不可取的,因此要么超过一定大小的回拨直接报错,拒绝服务,或者有一种方案是利用拓展位,回拨之后在拓展位上加1就可以了,这样ID依然可以保持唯一。但是这个要求我们提前预留出位数,要么从机器id中,要么从序列号中,腾出一定的位,在时间回拨的时候,这个位置
+1
。
相关问题
1. 第一位为什么不使用?
在计算机的表示中,第一位是符号位,0表示整数,第一位如果是1则表示负数,我们用的ID默认就是正数,所以默认就是0,那么这一位默认就没有意义。
2.机器位怎么用?
机器位或者机房位,一共10 bit,如果全部表示机器,那么可以表示1024台机器,如果拆分,5 bit 表示机房,5bit表示机房里面的机器,那么可以有32个机房,每个机房可以用32台机器。
3. twepoch表示什么?
由于时间戳只能用69年,我们的计时又是从1970年开始的,所以这个twepoch
表示从项目开始的时间,用生成ID的时间减去twepoch
作为时间戳,可以使用更久。
4. -1L ^ (-1L << x) 表示什么?
表示 x 位二进制可以表示多少个数值,假设x为3:
在计算机中,第一位是符号位,负数的反码是除了符号位,1变0,0变1, 而补码则是反码+1:
1 2 3
| -1L 原码:1000 0001 -1L 反码:1111 1110 -1L 补码:1111 1111
|
从上面的结果可以知道,-1L其实在二进制里面其实就是全部为1,那么 -1L 左移动 3位,其实得到 1111 1000
,也就是最后3位是0,再与-1L
异或计算之后,其实得到的,就是后面3位全是1。
-1L ^ (-1L << x)
表示的其实就是x位全是1的值,也就是x位的二进制能表示的最大数值。
5.时间戳比较
在获取时间戳小于上一次获取的时间戳的时候,不能生成ID,而是继续循环,直到生成可用的ID,这里没有使用拓展位防止时钟回拨。
6.前端直接使用发生精度丢失
如果前端直接使用服务端生成的 long 类型 id,会发生精度丢失的问题,因为 JS 中Number是16位的(指的是十进制的数字),而雪花算法计算出来最长的数字是19位的,这个时候需要用 String 作为中间转换,输出到前端即可。