Twitter的雪花算法(snowflake)自增ID

发布时间 2023-12-28 09:05:53作者: 阿风小子

前言
  这个问题源自于,我想找一个分布式下的ID生成器。
  这个最简单的方案是,数据库自增ID。为啥不用咧?有这么几点原因,一是,会依赖于数据库的具体实现,比如,mysql有自增,oracle没有,得用序列,mongo似乎也没有他自己有个什么ID,sqlserver貌似有自增等等,有些不稳定因素,因为ID生成是业务的核心基础。当然,还有就是性能,自增ID是连续的,它就依赖于数据库自身的锁,所以数据库就有瓶颈。当然了,多台数据库加某种间隔也是可用的,但是,运维维护会很复杂,因为它不是内聚的解决方案。而且,很难提前获得下一个ID。
  后来,我用过一段时间在数据库表里进行记录来进行自增。这个的优势是,我可以提前获得下一个ID,而且,某个进程里可以一次获取一批,减少锁的依赖,虽然进程间的不重复依然是基于数据库事务隔离的,但是,依赖小了,瓶颈小了。这个方案其实挺好的,我依然也会继续用,主要是,它可以生成数字字母混合的编剧号,而且基本可控。但是,我数据库主键为了效率和空间成本,基本会选用long,基本顺序生成就可以了,所以,使用这种带持久化的方案,会显得很重。起项目的时候,也是,需要先建立对应的表,然后再把代码或者jar包引进去,然后再用,比较重。最好就是能够直接生成,没有那么多依赖。
  然后,我从我上司那里听到了twitter的这个算法。其实,我上司有个实现,我这个就是基于他的改的,但是,他的有两个值是配置的,我还是嫌麻烦,于是就动手把那两个值变成了从机器与进程获取,就有了这个版本。

思路
  说实话,我也就听了这么个算法的名字,没正经看过原算法,但是,我上司说他代码是网上抄的,所以,这个算法名字我还是不敢丢,下面我们说说整体的思路。
  整个ID的构成大概分为这么几个部分,时间戳差值,机器编码,进程编码,序列号。java的long是64位的从左向右依次介绍是:时间戳差值,在我们这里占了42位;机器编码5位;进程编码5位;序列号12位。所有的拼接用位运算拼接起来,于是就基本做到了每个进程中不会重复了。

代码

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
package nature.framework.core.common;
 
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
 
/**
 * 主键生成器
 *
 * @author nature
 * @create 2017-12-22 10:58
 */
public class KeyWorker {
    private final static long twepoch = 12888349746579L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
 
    // 毫秒内自增位数
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    //sequence掩码,确保sequnce不会超出上限
    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    //上次时间戳
    private static long lastTimestamp = -1L;
    //序列
    private long sequence = 0L;
    //服务器ID
    private long workerId = 1L;
    private static long workerMask= -1L ^ (-1L << workerIdBits);
    //进程编码
    private long processId = 1L;
    private static long processMask=-1L ^ (-1L << datacenterIdBits);
    private static KeyWorker keyWorker = null;
 
    static{
        keyWorker=new KeyWorker();
    }
    public static synchronized long nextId(){
        return keyWorker.getNextId();
    }
 
    private KeyWorker() {
 
        //获取机器编码
        this.workerId=this.getMachineNum();
        //获取进程编码
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();
 
        //避免编码超出最大值
        this.workerId=workerId & workerMask;
        this.processId=processId & processMask;
    }
 
    public synchronized long getNextId() {
        //获取时间戳
        long timestamp = timeGen();
        //如果时间戳小于上次时间戳则报错
        if (timestamp < lastTimestamp) {
            try {
                throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        //如果时间戳与上次时间戳相同
        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
        return nextId;
    }
 
    /**
     * 再次获取时间戳直到获取的时间戳与现有的不同
     * @param lastTimestamp
     * @return 下一个时间戳
     */
    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }
 
    private long timeGen() {
        return System.currentTimeMillis();
    }
 
    /**
     * 获取机器编码
     * @return
     */
    private long getMachineNum(){
        long machinePiece;
        StringBuilder sb = new StringBuilder();
        Enumeration<NetworkInterface> e = null;
        try {
            e = NetworkInterface.getNetworkInterfaces();
        catch (SocketException e1) {
            e1.printStackTrace();
        }
        while (e.hasMoreElements()) {
            NetworkInterface ni = e.nextElement();
            sb.append(ni.toString());
        }
        machinePiece = sb.toString().hashCode();
        return machinePiece;
    }
}

  

代码解读
整体设计
  为了最大程度的减少配置,方便实用,这个模块,我设计成了单例模式。之所以没有直接使用static方法,还是希望可以控制整个模块的生命周期,但是,模块的初始化,我使用了static块,因为它没有任何依赖。
  有个static的nextId方法,可以直接获得下一个ID,这个方法是线程安全的。同时这个模块的使用就是这么简单粗暴,也不用配置bean。

ID生成逻辑
  我们先看最后一步:long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
  这句话什么意思呢?
  timestamp - twepoch:时间戳减去一个时间戳,获得一个差值。
  ((timestamp - twepoch) << timestampLeftShift):timestampLeftShift是22,这个操作是将这个差值向左移22位,左移空出来的会自动补0,我们就有了22位的空间了。
  后面可以看到三个|符号,与操作会把1都加进来,而我们后面的数也都在各自的位上才有1,那么|操作就把这些数合进来了。
  (processId << datacenterIdShift):进程编码左移datacenterIdShift,这个是17位,而processId最多是5位,于是刚好填满空位
  (workerId << workerIdShift):与进程编码类似,机器编码也是5位,左移12位
  sequence最大12位。

如何确保不超出位数限制
  前面的逻辑中,我们说了很多不超出位数限制啥的内容,那么,具体是怎么做到的呢?我们拿workerId举个例子:
  this.workerId=workerId & workerMask;
  这是我们确保workerId不超过5位的语句,什么意思呢?不经常操作位运算真看不懂。我们先看看workerMask是啥。
  private static long workerMask= -1L ^ (-1L << workerIdBits);
  。。。什么意思呀?它先执行的是-1L << workerIdBits,workerIdBits是5。这又是什么意思呢?注意,这是位运算,long用的是补码,-1L,就是64个1,这里使用-1是为了格式化所有位数,<<是左移运算,-1L左移五位,低位补零,也就是左移空出来的会自动补0,于是就低位五位是0,其余是1。然后^这个符号,是异或,也是位运算,位上相同则为0,不通则为1,和-1做异或,则把所有的0和1颠倒了一下。这时候,我们再看,workerId & workerMask,与操作,两个位上都为1的才能唯一,否则为零,workerMask高位都是0,所以,不管workerId高位是什么,都是0,;而workerMask低位都是1,所以,不管workerId低位是什么,都会被保留,于是,我们就控制了workerId的范围。

最后的异常
  这里,时间戳,保证了不通毫秒不同,然后机器编码进程编码保证了不同进程不通,再然后,序列,在统一毫秒内,如果获取第二个ID,则序列号+1,到下一毫秒后重置。至此,唯一性ok。但是,还有问题,序列号用完了怎么办?代码里的解决方案是,等到下一毫秒。

补充
  其实,这个方案中,机器码和进程编码是可能相同的,只是概率比较小,我们就凑合着用吧。如果有更好地获取这两位的方式,欢迎沟通。