SnowFlake算法及其JAVA源码分析

    在订单系统中,当消费者成功下单后,都会生成一个用于唯一识别的订单编号。目前,大多数的服务系统都为分布式系统,如何在分布式系统中生成具有一定顺序的全局唯一编号,既方便用作唯一标识,又方便数据库存储和索引,SnowFlake算法就派上用场啦。
    为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同,于是,Twitter公司推出了SnowFlake方法,目前被各互联网公司广泛使用。

1.SnowFlake序号结构

在这里插入图片描述
    SnowFlake所生成的ID一共分成四部分:

1.第一位占用1bit,其值始终是0,没有实际作用;
2.时间戳占用41bit,精确到毫秒,总共可以容纳约140年的时间;
3.工作机器id占用10bit,其中高位5bit是数据中心ID(DATACENTER),低位5bit是工作节点ID(MACHINE),也就是说,该算法可以容纳1024个节点的集群;
4.序列号占用12bit,也就是说,同一个节点一毫秒内可以容纳4095次请求或操作。

    SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?只需要做一个简单的乘法:同一毫秒的ID数量 = 1024 X 4096 = 4194304。这个数字在绝大多数并发场景下都是够用的。

2.SnowFlake时间戳

    在基于JAVA的SnowFlake中,我们发现算法的时间戳是一个本地方法,即他是调用本地函数来实现的,目的是获取当前的毫秒数字,这个数字是不会以数据中心或节点的改变而变化的,而只以客观的时间变化而变化。
    JAVA中,获取时间戳的方法为getNewstmp,可以看到代码如下:

    private long getNewstmp() {
        return System.currentTimeMillis();
    }

而System类中的currentTimeMillis方法如下:

    public static native long currentTimeMillis();

3.SnowFlake工作机器ID

    根据笔者的理解,在JAVA源码中,工作机器ID号是需要人为设定的,需要根据代码处在不同的服务器上设置不同的编号,比如,可以使用MAC或IP地址来唯一标示工作机器,如果工作机器比较少,也可以使用配置文件来直接设置ID。它的值是SnowFlake类在构造时确定的,在new这个类时,需要我们传入工作机器ID的参数。
    SnowFlake类的构造方法如下:

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

可以看到,在new一个SnowFlake类时,需要我们传入数据中心ID号和机器ID号,用于唯一标识我们分布式集群中的某一台服务器,至于具体ID号的生成办法,需要使用者自己去确定,而该方法本身不会给出具体方案。

4.SnowFlake序列号

    当我们确定了秒级为单位的时间、当前使用的工作机器ID号,接下来要关注的点就是该台机器在1ms内的并发量,这就是序列号的含义。
    简单来说,序列号就是一系列的自增ID,为了给同一毫秒内的多条消息按时间顺序分配唯一识别ID,若同一毫秒把序列号用完了,则“等待至下一毫秒”,这个就有点类似令牌桶的思想。
    SnowFlake生成序号的方法叫nextId,其代码如下:

    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

其中,sequence即代表序列号,方法中间部分即为生成序列号的过程,在return部分,返回秒级时间戳+工作机器ID+序列号,就是我们所需要的自增全局唯一ID啦。
    另外,需要注意的是该方法采用了synchronized来控制并发,这是考虑到一定会有很多的高并发请求来生成序列号,在这种背景下,用悲观锁要比用乐观锁更合适。由于该方法是线程安全的,即同一时刻只会有一个线程在该方法中,因此,如果该线程获取到的序列号超过了最大值,也只会有这一个线程在方法内阻塞,直到下一毫秒到来后,该线程的序列号被置为下一毫秒的0号。
    其中,阻塞的方法为getNextMill(),其源码为:

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

可以看到,实际上就是当前线程不断轮询获取当前时刻,直到下一毫秒的到来,并且返回下一毫秒。

5.优缺点

算法优点:

1.生成ID时不依赖于DB,完全在内存生成,高性能高可用。

2.ID呈趋势递增,后续插入索引树的时候性能较好。

算法缺点:

依赖于系统时钟的一致性。如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值