一篇文章掌握雪花算法

一.什么是雪花算法

Snowflake算法是Twitter开源的分布式ID生成算法。核心思想是,使用一个64 bit 的 long 类型的数字作为全局唯一ID。算法中还引入了时间戳,基本保证了自增特性。

二.为什么需要 Snowflake

在分布式系统中,如果使用的是数据库的自增ID,会出现这些问题:

  • 多个节点无法共享同一自增序列

    数据库的自增 ID 通常是由单个数据库实例内部维护的。当系统演化为分布式架构,部署了多个数据库或服务节点时,各节点的自增计数器相互独立,都会从 1 开始递增。这样会导致不同节点生成的 ID 出现重复,从而无法保证全局唯一性。

  • 数据库成为性能瓶颈

    如果让所有服务节点都访问同一个数据库的自增序列来获取 ID,那么每次生成 ID 都需要一次数据库访问。随着并发量提升,数据库就会成为全局的性能瓶颈,甚至在高并发场景下可能因过载而崩溃。

  • ID 不具备时间含义

    数据库自增 ID 虽然是递增的,但只反映插入顺序,而不包含任何时间信息。因此,当需要根据 ID 判断数据生成时间或顺序(例如订单创建顺序、日志时间顺序)时,无法直接实现。

Snowflake 可以让多个节点独立生成唯一 ID,不需要依赖中心数据库。

三.Snowflake ID的结构

Snowflake算法生成ID的结果是一个64bit大小的整数,结构如下:

  • 第一部分:1个bit,无意义,固定为0。二进制中最高位是符号位,1表示负数,0表示整数。ID都是正数,所以固定为0。

  • 第二部分:41个bit,表示相对于某个基准时间的毫秒数,用于体现时间顺序。时间戳部分具备天然的自增特性,是整个 ID 的主排序依据。

  • 第三部分:10个bit,表示10位的机器标识,最多支持2^10=1024个节点。一般分为5位datacenterId和5位workerIddatacenterId表示数据中心ID,workerId表示机器ID。

  • 第四部分:12个bit,表示同一毫秒内的自增序列号,通过这个递增的序列号区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成2^12=4096个不重复的ID。

四.Snowflake算法特性

由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的ID就是long来存储的。

优点

  • 高并发分布式环境下生成不重复ID,每个节点都有独立标识,通过时间戳+机器号+序列号组合生成唯一ID。每秒可以生成百万个不重复ID

  • 基于时间戳,以及同一时间戳下序列号自增,基本保证ID有序递增

  • 不依赖第三方库或者中间件

  • 算法简单,在内存中进行,效率高

缺点

依赖服务器时间,当服务器发生时钟回拨(即系统时间被人为调整或出现异常跳变)时,可能导致生成的时间戳比上一次生成ID的时间戳更小,从而造成ID重复的风险。算法中可通过记录最后一个生成ID时的时间戳来解决,在每次生成新ID之前比较当前服务器时钟是否被回拨,避免生成重复ID。

  • 若当前时间 ≥ 上次时间戳,则正常生成ID;

  • 若当前时间 < 上次时间戳(即发生了时钟回拨),则采取容错策略,如:

    • 等待时钟追上(自旋等待)

      while (timestampNow < this.lastTimestamp) {
           timestampNow = System.currentTimeMillis();
      }

      实现简单不引入额外的复杂逻辑和状态,但若回拨时间较长,线程会一直阻塞,对高并发服务很不友好,会非常容易造成线程阻塞或雪崩。

    • 抛出异常并暂停服务

    • 通过引入额外的时间偏移量来校正

五.Snowflake算法实现

算法说明

  • 自动识别机器信息(IP、主机名)生成dataCenterId和WorkerId

  • 支持手动传参自定义节点标识

  • 加入时钟回拨容错机制

  • 生成Id保证线程安全(synchronized)

1、关键参数配置

起始时间戳、每部分所需bit位、机器表示部分和序列号、每一部分向左的偏移量(用于拼接id)

2、构造函数

提供无参构造和有参构造两种方式

3、自动获取数据中心ID和机器ID

getDataCenterId()getWorkerId() 方法

4、判断用户输入数据中心ID和机器ID是否合法

isVaildDataCenterId()isVaildWorderId() 方法

5、雪花算法核心方法

nextId(),该方法通过时间戳、数据中心ID、机器ID和序列号的组合,生成全局唯一的64位长整型ID,确保在分布式系统中不同节点生成的ID不重复。

核心流程:

  1. 获取当前时间戳

    使用 System.currentTimeMillis() 获取当前毫秒时间。

  2. 检测时钟回拨

    若当前时间小于上次生成ID的时间(即发生时钟回拨),根据回拨的时间差(timestampOffset)采取不同的容错策略:

    • ≤5ms:短暂回拨,自旋等待。

    • ≤1000ms:中度回拨,通过时间偏移补偿。

    • 1000ms:严重回拨,直接抛出异常中止服务。

  3. 序列号处理

    • 若当前时间与上次相同(同一毫秒内),序列号自增,避免ID冲突。

    • 若序列号达到最大值,则等待进入下一毫秒再继续生成。

    • 若时间不同(进入下一毫秒),序列号重置为0。

  4. 更新时间戳记录

    将当前时间戳保存为 lastTimestamp,用于下次判断是否跨毫秒。

  5. 通过位移与或运算,将各个部分拼接为最终ID

public class Snowflake {

    /*
        起始时间戳 2003-01-11 00:00:00(自定义)
     */
    private static final long START_TIMESTAMP = 1042214400000L;

    /*
        时间戳部分,占用41位
     */
    private static final long TIMESTAMP_BIT = 41L;

    /*
        机器标识,占用10位
        分为两部分 dateCenter 和 worker
     */
    private static final long DATECENTER_BIT = 5L;
    private static final long WORKER_BIT = 5L;

    /*
        序列号,占用12位
     */
    private static final long SEQUENCE_BIT = 12L;


    /*
        机器标识部分、序列号最大值
        -1L表示long类型的每一位都是1
        左移,相当于右边补几个0
        ~表示取反,就得到了最大值
     */
    private static final long MAX_DATECENTER_ID = ~(-1L << DATECENTER_BIT);
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_BIT);
    private static final long MAX_SEQUENCE_ID = ~(-1L << SEQUENCE_BIT);


    // 数据中心id
    private final long dataCenterId;
    // 机器id
    private final long workerId;
    // 序列号,毫秒内序列
    private long sequenceId = 0L;
    // 上一次生成id的时间戳
    private long lastTimestamp = -1L;

    // 时间戳偏移量
    private long timestampOffset = 0L;

    /*
        每一部分向左的位移
     */
    private final static long WORKER_LEFT = SEQUENCE_BIT;
    private final static long DATECENTER_LEFT = WORKER_LEFT + WORKER_BIT;
    private final static long TIMESTAMP_LEFT = DATECENTER_LEFT + DATECENTER_BIT;

    /**
     * 无参构造函数
     * 自动获取数据中心id和机器id,不需要使用方传参
     */
    public Snowflake() throws UnknownHostException {
        this.dataCenterId = getDataCenterId();
        this.workerId = getWorkerId();
    }

    /**
     * 提供有参构造函数,可以自己传递数据中心id和机器id
     * 如果传递的数据中心id或机器id超出范围或者不合法,则使用自动获取
     *
     * @param dataCenterId 数据中心id
     * @param workerId     机器id
     */
    public Snowflake(long dataCenterId, long workerId) throws UnknownHostException {
        this.dataCenterId = isVaildDataCenterId(dataCenterId) ? dataCenterId : getDataCenterId();
        this.workerId = isVaildWorderId(workerId) ? workerId : getWorkerId();
    }

    /**
     * 判断数据中心id是否合法
     * 是否超出最大值或不大于0
     *
     * @param dataCenterId 数据中心id
     * @return true/false
     */
    private boolean isVaildDataCenterId(long dataCenterId) {
        return dataCenterId <= MAX_DATECENTER_ID && dataCenterId > 0;
    }

    /**
     * 判断机器id是否合法
     * 是否超出最大值或不大于0
     *
     * @param workerId 机器id
     * @return true/false
     */
    private boolean isVaildWorderId(long workerId) {
        return workerId <= MAX_WORKER_ID && workerId > 0;
    }


    /**
     * 获取数据中心id
     * 获取机器的ip地址,截取ip地址的末两位,并转换为long类型
     * 截取的ip地址末两位可能为0,所以需要判断
     * 将获取的ipNum与MAX_DATECENTER_ID进行与运算,保证不超过MAX_DATECENTER_ID
     *
     * @return 数据中心id
     */
    private static long getDataCenterId() throws UnknownHostException {
        String ip = InetAddress.getLocalHost().getHostAddress();
        char[] ipCharArray = ip.toCharArray();
        // 取IP地址的后两位,取非0的
        long ipNum = 0L;
        int count = 1;
        for (int i = ip.length() - 1; i >= 0; i--) {
            if (ipCharArray[i] != '.' && ipCharArray[i] != '0') {
                ipNum += (long) ((ipCharArray[i] - '0') * Math.pow(10, count));
                if (count++ == 2) {
                    break;
                }
            }
        }
        return ipNum & MAX_DATECENTER_ID;
    }

    /**
     * 获取机器id
     * 获取主机名,并计算为hashCode
     * 将获取的hashCode与MAX_WORKER_ID进行与运算,保证不超过MAX_WORKER_ID
     *
     * @return 机器id
     */
    private static long getWorkerId() throws UnknownHostException {
        // 获取主机名
        String hostName = InetAddress.getLocalHost().getHostName();
        return hostName.hashCode() & MAX_WORKER_ID;

    }


    /**
     * 生成id————雪花算法
     * 时间戳+数据中心Id+机器Id+序列号
     * 时间戳部分:41bit位,从起始时间戳开始,当前时间戳减去起始时间戳
     * 数据中心Id部分:5bit位,使用方传递或者自动获取
     * 机器Id部分:5bit位,使用方传递或者自动获取
     * 序列号部分:12bit位,从0开始,每毫秒内自增1
     * 最后将这些part拼接到一起,采用移位运算拼接组成一个long类型的Id
     *
     * @return 返回生成的雪花id
     */
    public synchronized long nextId() {
        // 获取当前时间戳
        long timestampNow = System.currentTimeMillis();
        // 发生了时钟回拨
        if (timestampNow < this.lastTimestamp) {
            // 计算偏移量,来判断要采用哪种容错策略
            this.timestampOffset = this.lastTimestamp - timestampNow;
            if (this.timestampOffset <= 5) { // 小于等于5ms
                // 自旋等待
                try {
                    Thread.sleep(this.timestampOffset << 1); // 等待2倍偏移量
                    timestampNow = System.currentTimeMillis() + this.timestampOffset;
                } catch (InterruptedException e) {
                    throw new RuntimeException("时钟回拨,等待后未恢复");
                }
            } else if (this.timestampOffset <= 1000) {
                // 中度回拨,增加偏移量
                this.timestampOffset += this.lastTimestamp - timestampNow;
                timestampNow = timestampNow + this.timestampOffset;
            } else {
                // 回拨时间大于1s,严重回拨,直接抛出异常中断服务
                throw new RuntimeException("时钟回拨了" + this.timestampOffset + "毫秒,服务终止!");
            }

        }
        // 判断处理生成序列号
        if (timestampNow == this.lastTimestamp) {
            // 同一毫秒内,序列号自增(并保证不超过最大值)
            this.sequenceId = (this.sequenceId + 1) & MAX_SEQUENCE_ID;
            if (this.sequenceId == 0) {// 同一毫秒内序列号已满,等待下一毫秒
                while (timestampNow <= this.lastTimestamp) {
                    timestampNow = System.currentTimeMillis();
                }
            }
        } else {
            // 不同毫秒内,序列号置为0
            this.sequenceId = 0L;
        }
        // 记录当前时间戳,作为下一次生成id时间戳的判断依据
        this.lastTimestamp = timestampNow;
        // 拼接id,或运算、左移
        return (timestampNow - START_TIMESTAMP) << TIMESTAMP_LEFT
                | (dataCenterId << DATECENTER_LEFT)
                | (workerId << WORKER_LEFT)
                | (sequenceId);
    }


    public static void main(String[] args) throws UnknownHostException {
        // 测试
        int i = 20;
        Snowflake snowflake = new Snowflake();
        while (i-- > 0) {
            System.out.println(snowflake.nextId());
        }
    }

}

需要注意的是:每次new Snowflake(),会创建一个新的Snowflake 实例。这会导致时间戳(lastTimestamp)会重新初始化为 -1,会使不同的Snowflake 对象创建出来的Id会有重复的。在spring项目中,应将Snowflake交由spring容器统一管理,将其注册成为一个单例Bean,并在需要使用的地方采取依赖注入(@Autowired)的方式使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

他们都叫我0xCAFEBABE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值