最近看到了一篇分析雪花算法的文章还不错,然后整理了一下分享出来。
先来科普一下SnowFlake算法
算法原理 Twitter Snowflake 生成的 unique ID 的组成 (由高位到低位):
41 bits: Timestamp (毫秒级) 10 bits: 节点 ID (datacenter ID 5 bits + worker ID 5 bits) 12 bits: sequence number 一共 63 bits (最高位是 0).
| 0(最高位预留) | 时间戳(41位) | 机器ID(10位) | 自增序列(12位) | unique ID 生成过程:
10 bits 的机器号, 在 ID 分配 Worker 启动的时候,从一个 Zookeeper 集群获取 (保证所有的 Worker 不会有重复的机器号);
41 bits 的 Timestamp: 每次要生成一个新 ID 的时候,都会获取一下当前的 Timestamp, 然后分两种情况生成 sequence number;
如果当前的 Timestamp 和前一个已生成 ID 的 Timestamp 相同 (在同一毫秒中),就用前一个 ID 的 sequence number + 1 作为新的 sequence number (12 bits); 如果本毫秒内的所有 ID 用完,等到下一毫秒继续 (这个等待过程中, 不能分配出新的 ID);
如果当前的 Timestamp 比前一个 ID 的 Timestamp 大, 随机生成一个初始 sequence number (12bits) 作为本毫秒内的第一个 sequence number;
41-bit的时间可以表示(1L<<41)/(1000L x 3600 x 24 x 365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
优缺点这里就不赘述了。
那我们继续看一个经典的Java版本的实现,这个在网上一搜一大把,官方原版的Scala版本
public class Snowflake { private static final Logger logger = LoggerFactory.getLogger(Snowflake.class); /** * 机器ID */ private final long workerId; /** * 时间起始标记点,作为基准,一般取系统的最近时间,默认2017-01-01 */ private final long epoch = 1483200000000L; /** * 机器id所占的位数(源设计为5位,这里取消dataCenterId,采用10位,既1024台) */ private final long workerIdBits = 10L; /** * 机器ID最大值: 1023 (从0开始) */ private final long maxWorkerId = -1L ^ -1L << this.workerIdBits; /** * 机器ID向左移12位 */ private final long workerIdShift = this.sequenceBits; /** * 时间戳向左移22位(5+5+12) */ private final long timestampLeftShift = this.sequenceBits + this.workerIdBits; /** * 序列在id中占的位数 */ private final long sequenceBits = 12L; /** * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095),12位 */ private final long sequenceMask = -1L ^ -1L << this.sequenceBits; /** * 并发控制,毫秒内序列(0~4095) */ private long sequence = 0L; /** * 上次生成ID的时间戳 */ private long lastTimestamp = -1L; private final int HUNDRED_K = 100_000; /** * @param workerId 机器Id */ private Snowflake(long workerId) { if (workerId > this.maxWorkerId || workerId < 0) { String message = String.format("worker Id can't be greater than %d or less than 0", this.maxWorkerId); throw new IllegalArgumentException(message); } this.workerId = workerId; } /** * Snowflake Builder * @param workerId workerId * @return Snowflake Instance */ public static Snowflake create(long workerId) { return new Snowflake(workerId); } /** * 批量获取ID * @param size 获取大小,最多10万个 * @return SnowflakeId */ public long[] nextId(int size) { if (size <= 0 || size > HUNDRED_K) { String message = String.format("Size can't be greater than %d or less than 0", HUNDRED_K); throw new IllegalArgumentException(message); } long[] ids = new long[size]; for (int i = 0; i < size; i++) { ids[i] = nextId(); } return ids; } /** * 获得ID * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); // 如果上一个timestamp与新产生的相等,则sequence加一(0-4095循环); if (this.lastTimestamp == timestamp) { // 对新的timestamp,sequence从0开始 this.sequence = this.sequence + 1 & this.sequenceMask; // 毫秒内序列溢出 if (this.sequence == 0) { // 阻塞到下一个毫秒,获得新的时间戳 timestamp = this.tilNextMillis(this.lastTimestamp); } } else { // 时间戳改变,毫秒内序列重置 this.sequence = 0; } // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 if (timestamp < this.lastTimestamp) { String message = String.format("Clock moved backwards. Refusing to generate id for %d milliseconds.", (this.lastTimestamp - timestamp)); logger.error(message); throw new RuntimeException(message); } this.lastTimestamp = timestamp; // 移位并通过或运算拼到一起组成64位的ID return timestamp - this.epoch << this.timestampLeftShift | this.workerId << this.workerIdShift | this.sequence; } /** * 等待下一个毫秒的到来, 保证返回的毫秒数在参数lastTimestamp之后 * @param lastTimestamp 上次生成ID的时间戳 * @return */ private long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 获得系统当前毫秒数 */ private long timeGen() { return System.currentTimeMillis(); } }
那让我们看一下代码来理解一下算法的细节。
代码理解 我们从关键的代码段来理解,如下:
this.sequence = this.sequence + 1 & this.sequenceMask; private final long maxWorkerId = -1L ^ -1L << this.workerIdBits; return ((timestamp - this.epoch) << this.timestampLeftShift) | (this.workerId << this.workerIdShift) | this.sequence;
ps. 我这里取消了datacenterId,将datacenterId和workerid合并到workerIdBits
负数的二进制表示 在计算机中,负数的二进制是用补码来表示的。 假设我是用Java中的int类型来存储数字的, int类型的大小是32个二进制位(bit),即4个字节(byte)。(1 byte = 8 bit) 那么十进制数字3在二进制中的表示应该是这样的:
00000000 00000000 00000000 00000011 // 3的二进制表示,就是原码
那数字-3在二进制中应该如何表示? 我们可以反过来想想,因为-3+3=0, 在二进制运算中把-3的二进制看成未知数x来求解, 求解算式的二进制表示如下:
00000000 00000000 00000000 00000011 //3,原码 + xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx //-3,补码 ----------------------------------------------- 00000000 00000000 00000000 00000000
反推x的值,3的二进制加上什么值才使结果变成00000000 00000000 00000000 00000000?:
00000000 00000000 00000000 00000011 //3,原码 + 11111111 11111111 11111111 11111101 //-3,补码 ----------------------------------------------- 1 00000000 00000000 00000000 00000000
反推的思路是3的二进制数从最低位开始逐位加1,使溢出的1不断向高位溢出,直到溢出到第33位。然后由于int类型最多只能保存32个二进制位,所以最高位的1溢出了,剩下的32位就成了(十进制的)0。
补码的意义就是可以拿补码和原码(3的二进制)相加,最终加出一个“溢出的0”
以上是理解的过程,实际中记住公式就很容易算出来:
补码 = 反码 + 1 补码 = (原码 - 1)再取反码 因此-1的二进制应该这样算:
00000000 00000000 00000000 00000001 //原码:1的二进制 11111111 11111111 11111111 11111110 //取反码:1的二进制的反码 11111111 11111111 11111111 11111111 //加1:-1的二进制表示(补码)
具体对位运算以及二进制的计算理解可以看看这篇文章https://blog.csdn.net/cj2580/article/details/80980459