雪花算法的原理以及实现

admin2024-07-01  6

文章目录

  • 一、简介
  • 二、算法优缺点
  • 三、算法实现

一、简介

有这么一种说法,自然界中并不存在两片完全一样的雪花的。每一片雪花都拥有自己漂亮独特的形状、独一无二。雪花算法也表示生成的ID如雪花般独一无二。

雪花算法 (SnowFlake )算法,是 Twitter 开源的分布式 id 生成算法

核心思想是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的。

这 64 个 bit 中,其中 1 个 bit 是不用的(我们生成的 id 都是正数,所以第一个 bit 统一都是 0),然后用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
雪花算法的原理以及实现,雪花算法,第1张

第一个部分,是 1 个 bit:0,这个是无意义的。

第二个部分是 41 个 bit:表示的是时间戳。

第三个部分是 5 个 bit:表示的是机房 id,10001。

第四个部分是 5 个 bit:表示的是机器 id,1 1001。

第五个部分是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 00000000。

①1 bit:是不用的,为啥呢?

因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

②41 bit:表示的是时间戳,单位是毫秒。

41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。

③10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。

但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),也可以根据自己公司的实际情况确定。

④12 bit:这个是用来记录同一个毫秒内产生的不同 id。

12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。

二、算法优缺点

雪花算法的优点:

(1)无依赖:不依赖第三方库或者中间件,完全在内存中生成,可用性强。

(2)高性能:每秒中能生成数百万的自增ID。

(3)ID自增:基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。

雪花算法的缺点:

依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。

三、算法实现

代码实现:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;


public class IdWorker {

    private static final Logger LOGGER = LoggerFactory.getLogger(IdWorker .class);

    private final static String ERROR_CLOCK_BACK = "时间回拨,拒绝为超出%d毫秒生成ID";

    private final static String ERROR_ATTR_LIMIT = "%s属性的范围为0-%d";


    /**
     * 用于用当前时间戳减去这个时间戳,算出偏移量
     */
    protected static final long TWEPOCH = 1538211907857L;

    /**
     * 机器id所占的位数(表示只允许workId的范围为:0-1023)
     */
    protected static final long WORKER_ID_BITS = 5L;

    /**
     * 数据标识id所占的位数
     */
    protected static final long DATACENTER_ID_BITS = 5L;

    /**
     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

    /**
     * 支持的最大数据标识id,结果是31
     */
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);

    /**
     * 序列在id中占的位数 (表示只允许sequenceId的范围为:0-4095)
     */
    protected static final long SEQUENCE_BITS = 12L;

    /**
     * 机器ID向左移12位
     */
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;

    /**
     * 数据标识id向左移17位(12+5)
     */
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    /**
     * 时间截向左移22位(5+5+12)
     */
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;

    /**
     * 生成序列的掩码,(防止溢出:位与运算保证计算的结果范围始终是 0-4095,0b111111111111=0xfff=4095)
     */
    private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);

    /**
     * 工作机器ID(0~31)
     */
    private long workerId;

    /**
     * 数据中心ID(0~31)
     */
    private long datacenterId;

    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;

    /**
     * 上次生成ID的时间截
     */
    private long lastTimestamp = -1L;

    public IdWorker () {
        this.datacenterId = getDataCenterId(MAX_DATACENTER_ID);
        this.workerId = getWorkerId(datacenterId, MAX_WORKER_ID);
    }


    public IdWorker (long workerId, long dataCenterId) {
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException(String.format(ERROR_ATTR_LIMIT, "workerId", MAX_WORKER_ID));
        }
        this.workerId = workerId;
        this.datacenterId = dataCenterId;
    }


    private static long getWorkerId (long dataCenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(dataCenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
            // GET jvmPid
            mpid.append(name.split("@")[0]);
        }
        // MAC + PID 的 hashcode 获取16个低位
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    private static long getDataCenterId(long tempMaxDataCenterId) {
        if (tempMaxDataCenterId < 0L || tempMaxDataCenterId > MAX_DATACENTER_ID) {
            tempMaxDataCenterId = MAX_DATACENTER_ID;
        }
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (tempMaxDataCenterId + 1);
            }
        } catch (Exception e) {
            LOGGER.warn("Get Data Center Id error, e:{}", e);
        }
        return id;
    }
    
    public synchronized long nextId() {
        long timestamp = timeGen();
        // 闰秒:如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    // 时间偏差大小小于5ms,则等待两倍时间
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        // 还是小于,抛异常并上报
                        throw new RuntimeException(String.format(ERROR_CLOCK_BACK, lastTimestamp - timestamp));
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            } else {
                throw new RuntimeException(String.format(ERROR_CLOCK_BACK, lastTimestamp - timestamp));
            }
        }

        // 解决跨毫秒生成ID序列号始终为偶数的缺陷:如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            // 通过位与运算保证计算的结果范围始终是 0-4095
            sequence = (sequence + 1) & SEQUENCE_MASK;
            // 毫秒内序列溢出
            if (sequence == 0) {
                // 阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 时间戳改变,毫秒内序列重置
            sequence = 0L;
        }

        // 上次生成ID的时间截
        lastTimestamp = timestamp;

        return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
                | (datacenterId << DATACENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

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

测试代码:

public static void main(String[] args) {
        IdWorker idWorker = new IdWorker();
        for (int i = 0; i < 10; i++){
            System.out.println(idWorker.nextId());
        }
    }

输出结果:

761722546083581952
761722546083581953
761722546083581954
761722546087776256
761722546087776257
761722546087776258
761722546087776259
761722546087776260
761722546087776261
761722546087776262

雪花算法的原理以及实现,在这里插入图片描述,第2张

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!