全局唯一id

74 阅读8分钟

全局 & 局部

全局是个什么概念?

我的理解:全局是个相对的概念,相对的全局

为什么这么说?

全世界所有计算机在一个系统里,这是不是全局的概念?

公司所有计算机在一个系统里,这又是一个全局的概念。

公司中的机器分出了10台给具体某一个独立完整的程序使用,这十台计算机也是个全局的概念。

有全局,那么自然就有局部的概念:我的理解是,每一台计算机都是局部概念的展现。

全局唯一id的设计方案

全局唯一id是指:在某个全局中的所有计算机产生的所有序列是独一无二的,不会产生重复的。

跟着这个概念来设计全局唯一id,那么必然是可行的。

方案一

生成uuid做全局唯一id。uuid的生成原理是跟每台计算机的mac地址相关的,任何两台电脑的网卡地址都是不一样的。 所以这个uuid可以作为全局唯一id的生成方案。

缺点就是uuid没有任何规律可言,同一个节点生成的uuid都是找不到任何规律的,而且是字符串类型。

方案二

单独拿一台计算机出来写一个接口,这个接口的内容很简单返回一个序列号。

伪代码:

    private static long seq = 0L;
    
    public static long getSeq(){
        return seq++;
    }

然后全局的所有计算机都访问这个节点的这个接口,从而获取唯一的id。

当然这样设计在高并发的情况下会出现问题,因为seq++并不是原子操作,通过使用long对应的原子类,或者简单粗暴的给getSeq()方法加上锁即可。

除了并发安全问题,还有单点故障,以及序列没有持久化的问题。

单机故障:部署集群解决。

序列持久化问题:将序列入库 或者 自己通过io流写到物理磁盘上。

为什么序列要持久化?如果这个生成序列的节点重启,那么序列又得从0开始,那么序列就重复了,就不是唯一的了。

方案三

用数据库实现全局唯一id,利用数据库的主键自增,全局所有计算机要生成唯一id都让这台数据库操作。

还是会存在一个问题:单点故障。数据库没有持久化问题,数据库的表文件本来就是持久化在磁盘上的。

解决方案:部署主从数据库解决单点故障。

方案四

利用redis的 incr 这个原子自增命令 来实现。

存在问题:单点故障、持久化。

解决方案:主从redis 解决单点故障;用aof、rdb策略进行持久化。

方案五

主角登场:雪花算法(snow flake)

雪花算法

这种实现方案是最先推出来的是推特。

主要思想:一个long类型的整型占8个字节,一个字节有8个比特位,那么一个long类型的整型总共占有64个比特位。

在java中不存在什么无符号整型,底层二进制的最高位是符号位,所以实际上一个long类型存储数据的部分只有63位。

image.png

还剩下63位,这63位得表示四部分信息:

1、拿出41位表示时间戳。41位时间戳可以表示69年。

这里是毫秒级别的时间戳。

计算代码:

    public static void main(String[] args) {
        long a = 1L;
        System.out.println((a << 41) / 1000 / 60 / 60 / 24 / 365);
    }

2、拿出5位表示机房号。5位可表示的最大机房数是  (1 << 5)= 32。

3、拿出5位表示计算机节点编号。5位可表示的最大节点编号是 ( 1 << 5) = 32。

4、拿出12位表示序列号。12位序列号可表示的最大编号是 (1 << 12)== 4096。 到此:41 + 5 + 5 + 12 = 63,一位都没浪费。

说下1-4的整体语意:在某个毫秒中,xx号机房中的xx号台计算机,生成了 xx 序列。(同一毫秒最多生成4096个序列

到这里雪花算法的主要思想已经说完了。下面来贴一段我用java实现的雪花算法,有详细的注释说明。

前置知识:

1、完整的理解雪花算法那63位分别表示什么含义。

2、理解 位运算: & 、 |、<<。
public class SnowFlake {


    // 用5位表示机房号
    public static final int ROOM_LEN = 5;

    // 用5位表示电脑号
    public static final int COMPUTER_LEN = 5;

    // 用12位表示序列号
    public static final int SEQ_LEN = 12;

    // 序列最大表示值
    public static final int SEQ_MAX_VALUE = (1 << SEQ_LEN) - 1;

    // 电脑号最大表示值
    public static final int COMPUTER_MAX_VALUE = (1 << COMPUTER_LEN) - 1;

    // 机房最大表示值
    public static final int ROOM_MAX_VALUE = (1 << ROOM_LEN) - 1;

    // 毫秒级别41位表示需要往左移动几位
    public static final int MILLIS_LEFT_LEN = SEQ_LEN + COMPUTER_LEN + ROOM_LEN;

    // 机房号需要往左移动几位
    public static final int ROOM_LEFT_LEN = SEQ_LEN + COMPUTER_LEN;

    // 电脑号需要往左移动几位
    public static final int COMPUTER_LEFT_LEN = SEQ_LEN;

    // 当前的毫秒
    private long millis = 0L;

    // 每一毫秒都从序列0开始
    private int seq = 0;

    // 房间号
    private long roomId;

    // 电脑号
    private long computerId;

    // 上一秒的时间表示
    private long lastMillis = 0;


    // 雪花算法的构造函数
    public SnowFlake(long roomId,long computerId){
        if (roomId > ROOM_MAX_VALUE || roomId < 0){
            throw new IllegalArgumentException("机房号不合法");
        }
        if (computerId > COMPUTER_MAX_VALUE || computerId < 0){
            throw new IllegalArgumentException("机器号不合法");
        }
        this.roomId = roomId;
        this.computerId = computerId;
    }

    // 得到下一毫秒
    private long getNextMillis(){
        long millis = System.currentTimeMillis();
        // 注意这里要强制性获取到下一毫秒
        while (millis <= lastMillis){
            millis = System.currentTimeMillis();
        }
        return millis;
    }

    // 对外调用的方案,核心实现
    public long getNextSeqId(){
        // 获取当前毫秒级时间戳
        long curMillis = System.currentTimeMillis();
        // 如果当前进来的时间戳大于之前保留的时间戳,那么在逻辑区里重置序列号为0
        if (curMillis > lastMillis){
            seq = 0;
            millis = curMillis;
        }
        // 如果当前进来的时间戳等于之前保留的时间戳,那么在逻辑区里是对序列号的操作
        else {
            // 序列号 + 1
            seq = seq + 1;
            // 同一毫秒已经生成了4096个序列了
            // 强制让其等待下一个毫秒再生成序列
            if ((seq & SEQ_MAX_VALUE) == 0){
                millis = getNextMillis();
                // 下一个毫秒级别的时间戳了,那么序列号重置为0,重新开始
                seq = 0;
            }
        }
        lastMillis = millis;
        return seq | (computerId << COMPUTER_LEFT_LEN)
                | (roomId << ROOM_LEFT_LEN)
                | (millis << MILLIS_LEFT_LEN);
    }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(4, 3);
        for (int i = 0; i < 16; i++) {
            System.out.println(snowFlake.getNextSeqId());
        }
    }

}

部分运行结果:

image.png

这里着重强调一个点,上面代码第79行的else里的逻辑块


  // 序列号 + 1
  seq = seq + 1;
  // 同一毫秒已经生成了4096个序列了
  // 强制让其等待下一个毫秒再生成序列
  if ((seq & SEQ_MAX_VALUE) == 0){
      millis = getNextMillis();
      // 下一个毫秒级别的时间戳了,那么序列号重置为0,重新开始
      seq = 0;
  }

走进else里说明是在同一毫秒级别还要产生序列。

    seq & seq_max_value  == 0 这个判断为什么表示这一毫秒中已经产生4096个序列了?

        (1) seq_max_value 是 4095,和刚刚说的4096为啥相差一个,因为我们是从seq = 0开始表示的。

        (2) 4095 的16位二进制表示是:0000 1111 1111 1111

        (3) 在最大的基础上加上一就是4096,那4096:0001 0000 0000 0000

        (4) 所以 4095 & 4096 表示为如下:

0000 1111 1111 1111
0001 0000 0000 0000
& 结果:
0000 0000 0000 0000

(5) 所以得强制等下一毫秒,让序列重置为0开始计算。

    小细节优化:让当前时间戳减去一个固定时间戳,这样可以让41位时间戳多表示几年。

分布式id

分布式id,全局没有提分布式id,在文章的最后我们来提一下。

    在分布式的架构中,分库、分表的情况下,我们要生成表的id,就可以用全局唯一id的思想,简单粗暴点就是直接用uuid;要是想稍微体现出一点技术含量的就采用雪花算法来生成分布式id。

    全局唯一id和分布式id是啥关系?非等价关系

    个人觉得全局唯一id是思想层面上的东西;而分布式id是业务层面的东西;分布式id可以用全局唯一id来实现。

总结

1、介绍了全局唯一id的概念。

2、介绍了全局唯一id的实现思路。

3、重点介绍了全局唯一id的雪花算法思想 & 实现代码,对核心代码进行了讲解。

4、介绍了分布式id和全局唯一id的关系。