分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。
理论说明
- 能满足高并发分布式系统环境下ID不重复
- 基于时间戳,可以保证基本有序递增(有些业务场景对这个又要求)
- 不依赖第三方的库或者中间件
- 生成效率极高
- 1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0
- 41位,用来记录时间戳(毫秒)。41位可以表示2^41−1个数字,因为要保证自增,不允许回调时间!
- 10位,用来记录工作机器ID。 可以部署在2^10=1024个节点
- 12位,序列号,用来记录同毫秒内产生的不同id。 12位可以表示的最大正整数是2^12−1=4095,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
额外的,可以在这41+10+12位中,取出一些连续的位,设置为特别需求的标志位,如业务ID啥的
1 2 3 4 5 6 7 8
| 1L == 0000 0000 0000 0001 -1L == 1111 1111 1111 1111,-1 的补码 为 1 的 反码 +1 -1L << MACHINE_SHIFT:-1 左移 10 位:高位舍弃,低位用 0 补齐,为 1111 1100 0000 0000 则: 1111 1111 1111 1111 ^ 1111 1100 0000 0000 --------------------- = 0000 0011 1111 1111
|
补充说明
- 当进入下一毫秒时,序列号不应直接置0,因为如果一个业务量恰好1ms生成一次,那出现的结果均为偶数,不利于分库分表,可以根据时间奇偶性,来设定 0 或 1 来改善奇偶性
- 项目的 START_STAMP 是不允许会退的,因为要保证 ID 自增且唯一,如果回退了,有可能会出现重复 ID,当发现时间回退时,应抛出异常
- 当同一毫秒生成数达到最大值时,不能继续生成,否则序列号必然重复,需要自旋等待下一毫秒
Java 代码
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
| package vip.housir;
public class SnowFlake {
private final static long START_STAMP = 1600074513000L;
private final static int SERIAL_LEN = 12;
private final static int MACHINE_LEN = 10;
private final static long MAX_MACHINE = -1L ^ (-1L << MACHINE_LEN);
private final static long MAX_SERIAL = -1L ^ (-1L << SERIAL_LEN);
private final static int MACHINE_SHIFT = SERIAL_LEN;
private final static int TIMESTAMP_SHIFT = MACHINE_LEN + SERIAL_LEN;
private long serialNum = 0L;
private long lastTimeStamp = -1L;
private final int machineId;
public SnowFlake(int machineId) { if (machineId > MAX_MACHINE) { throw new RuntimeException("Machine ID out of the range, MAX_MACHINE: " + MAX_MACHINE); } this.machineId = machineId; }
public synchronized long generateId() { long curStamp = getNewStamp();
if (curStamp < lastTimeStamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); }
if (curStamp == lastTimeStamp) { serialNum = (serialNum + 1) & MAX_SERIAL; if (serialNum == 0L) { curStamp = waitNextMill(); } } else { serialNum = curStamp & 1; }
lastTimeStamp = curStamp;
return ((curStamp - START_STAMP) << TIMESTAMP_SHIFT) | (machineId << MACHINE_SHIFT) | serialNum; }
private long waitNextMill() { long mill = getNewStamp(); while (mill <= lastTimeStamp) { mill = getNewStamp(); } return mill; }
private long getNewStamp() { return System.currentTimeMillis(); } }
|