雪花id详解

2年前 (2022) 程序员胖胖胖虎阿
311 0 0

1.1 概述

分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。
它是Twitter开源的由64位整数组成分布式ID,性能较高,并且在单机上递增

1.2分布式ID的特点

  • 全局唯一性

不能出现有重复的ID标识,这是基本要求。

  • 递增性

确保生成ID对于用户或业务是递增的。

  • 高可用性

确保任何时候都能生成正确的ID。

  • 高性能性

在高并发的环境下依然表现良好

1.3 结构

雪花id详解

  • snowflake的结构如下(每部分用-分开):
    0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

  • 第 1 位标识位:符号标识,由于ID一般是正数,第一位一般固定 0,第一位未使用

  • 后 41 位时间戳:毫秒级,可以存储起始时间到当前时间的时间戳的差值(取值范围是0~2^41 -1),最多可以使用约 69 年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

  • 后 10 位机器位:可以部署1024个节点,包含 5 位数据中心ID(取值范围是0~31)(dataCenterId)和 5 位工作机器ID(取值范围是0~31)(workerId)

  • 后 12 位序列位:毫秒内的计算。12 位的计数顺序号支持每个节点,每毫秒生成 4096 个序号

  • 总 64 位:Long,转换成字符串后长度最多19

  • 整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分)

  SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢:
  同一毫秒的ID数量 = 1024 X 4096 = 4194304

1.4源码

public class SnowflakeHelper {
    /**
     * 时间错位数
     */
    private static final int BIT_TIMESTAMP = 41;
    /**
     * 序列位数
     */
    private static final int BIT_SEQUENCE = 12;
    /**
     * 数据中心位数
     */
    private static final int BIT_DATA_CENTER = 5;
    /**
     * 工作机器位数
     */
    private static final int BIT_WORKER = 5;
    /**
     * 总位数
     */
    private static final int BIT_SNOWFLAKE = BIT_TIMESTAMP + BIT_DATA_CENTER + BIT_WORKER + BIT_SEQUENCE;

    /**
     * 最大序列值
     */
    private static final long MAX_SEQUENCE = ~(-1L << BIT_SEQUENCE);
    /**
     * 最大数据中心值
     */
    private static final long MAX_DATA_CENTER = ~(-1L << BIT_DATA_CENTER);
    /**
     * 最大工作机器值
     */
    private static final long MAX_WORKER = ~(-1L << BIT_WORKER);

    /**
     * 时间戳左移位数
     */
    private static final long LEFT_TIMESTAMP = BIT_DATA_CENTER + BIT_WORKER + BIT_SEQUENCE;
    /**
     * 数据中心左移位数
     */
    private static final long LEFT_DATA_CENTER = BIT_WORKER + BIT_SEQUENCE;
    /**
     * 工作机器左移位数
     */
    private static final long LEFT_WORKER = BIT_SEQUENCE;

    /**
     * 雪花ID开始时间,可以从起始时间开始使用 69 年
     */
    private final long START_TIMESTAMP;
    /**
     * 数据中心ID
     */
    private final long DATA_CENTER_ID;
    /**
     * 工作机器ID
     */
    private final long WORKER_ID;
    /**
     * 上一次的时间戳
     */
    private long latestTimestamp = 0L;
    /**
     * 序列
     */
    private long sequence = 0L;

    private long leftWorker = LEFT_WORKER;

    private long leftDataCenter = LEFT_DATA_CENTER;

    private long leftTimestamp = LEFT_TIMESTAMP;

    public SnowflakeHelper(long startTimeStamp, long dataCenterId, long workerId) {
        if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER) {
            throw new IllegalArgumentException("Data center id must in 0 ~ " + MAX_DATA_CENTER);
        }
        if (workerId < 0 || workerId > MAX_WORKER) {
            throw new IllegalArgumentException("Worker id must in 0 ~ " + MAX_WORKER);
        }
        this.START_TIMESTAMP = startTimeStamp;
        this.DATA_CENTER_ID = dataCenterId;
        this.WORKER_ID = workerId;
    }

    public SnowflakeHelper(long startTimeStamp, long dataCenterId, long workerId,
                           Integer bitTimestamp,
                           Integer bitDataCenterId,
                           Integer bitWorkerId,
                           Integer bitSequence) {
        this(startTimeStamp, dataCenterId, workerId);
        bitTimestamp = bitTimestamp != null ? bitTimestamp : BIT_TIMESTAMP;
        bitDataCenterId = bitDataCenterId != null ? bitDataCenterId : BIT_DATA_CENTER;
        bitWorkerId = bitWorkerId != null ? bitWorkerId : BIT_WORKER;
        bitSequence = bitSequence != null ? bitSequence : BIT_SEQUENCE;
        Assert.isTrue((bitTimestamp + bitDataCenterId + bitWorkerId + bitSequence) == BIT_SNOWFLAKE,
                String.format("[Snowflake] The total length of the snowflake ID must be 63 bit (timestamp %d, data center %d, worker %d, sequence %d).", bitTimestamp, bitDataCenterId, bitWorkerId, bitSequence));
        leftWorker = bitSequence;
        leftDataCenter = leftWorker + bitWorkerId;
        leftTimestamp = leftDataCenter + bitDataCenterId;
    }

    public synchronized long next() {
        long now = now();
        if (now < latestTimestamp) {
            throw new CommonException("Snowflake ID clock abnormal.");
        }
        if (now == latestTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0L) {
                now = nextTime();
            }
        } else {
            sequence = 0L;
        }
        latestTimestamp = now;
        // 时间戳位
        return (now - START_TIMESTAMP) << leftTimestamp
                // 数据中心位
                | DATA_CENTER_ID << leftDataCenter
                // 工作机器位
                | WORKER_ID << leftWorker
                // 序列位
                | sequence;
    }

    private long nextTime() {
        long now = now();
        while (now <= latestTimestamp) {
            now = now();
        }
        return now;
    }

    private long now() {
        return SystemClock.now();
    }
}

雪花id详解
我们需要传入的参数只有 只有
workerId – 终端ID
datacenterId – 数据中心ID
Redis下这两个参数的计算方式:

import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
public class SnowflakeInitiator {
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class SnowIdDto implements Serializable, Comparable<SnowIdDto> {
        /**
         * 注册时的时间戳
         */
        private Long timestamp;
        /**
         * 数据中心节点  0~31
         */
        private Integer dataCenterId;
        /**
         * 工作节点 0~31
         */
        private Integer workerId;
        @Override
        public int compareTo(SnowIdDto o) {
            long ex = this.timestamp - o.getTimestamp();
            return ex > 0 ? 1 : -1;
        }
    }
    private static final Integer DATA_SIZE = 32;
    private static final String[] RADIX_STR = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v"};
    private static Map<String, Integer> RADIX_MAP = new LinkedHashMap<>();
    private static final String SNOW = "RedisPrefix";
    static {
        for (int i = 0; i < DATA_SIZE; i++) {
            RADIX_MAP.put(RADIX_STR[i], i);
        }
    }
    /**
     * 计算雪花算法参数的新算法
     *
     * @param redisTemplate
     * @param appName
     * @return
     */
    private SnowIdDto calculateDataIdAndWorkId(RedisTemplate redisTemplate, String appName) {
        String key = SNOW + appName;
        RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        long andIncrement = redisAtomicLong.getAndIncrement();
        long result = andIncrement % (DATA_SIZE * DATA_SIZE);
        String str = StringUtils.leftPad(Integer.toString(Math.toIntExact(result), DATA_SIZE), 2, "0");
        Integer dataId = RADIX_MAP.get(str.substring(0, 1));
        Integer workId = RADIX_MAP.get(str.substring(1, 2));
        return new SnowIdDto(System.currentTimeMillis(), dataId, workId);
    }
}

运算方式:
雪花id详解

1.5 通用Mapper中如何集成雪花Id?

对于Mybatis来说,他提供自定义的生成主键的策略,提供了一个抽象的接口
雪花id详解
我们需要实现这个接口并且在Mybatis的拦截器里面调用他,去生产雪花Id
首先是一个mybatis的拦截器
雪花id详解

在拦截器中处理主键的id
雪花id详解
接着下一步
如果是Insert的语句我们有一个before的处理:
雪花id详解
![在这里插入图片描述](https://img-blog.csdnimg.cn/5bfcfb28364b4ace9923ccf9d7538bdb.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pe256m65oGL5peF5Lq6,size_20,color_FFFFFF,t_70,g_se,x_16hzero 提供了两种,mybatis也有默认的实现
![在这里插入图片描述](https://img-blog.csdnimg.cn/a5d768e4840f49f685ea53617bf8ae3f.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pe256m65oGL5peF5Lq6,size_20,color_FFFFFF,t_70,g_se,x_16然后我们进入雪花Id的Id生成器里面
雪花id详解
批量和非批量的都调用generateKey方法
雪花id详解
然后就会生成这个id
雪花id详解

自己写的MyBatis InsertSQLb并不会出现用到雪花Id,如果想要实现参考:https://blog.csdn.net/dai909237869/article/details/111176110

1.6 SnowFlake算法的缺点:

  • 依赖于系统时钟的一致性。如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序。
  • 还有,在启动之前,如果这台机器的系统时间回拨过,那么有可能出现ID重复的危险。

1.7问题?

workId 怎么保证唯一?

  1. 可以通过分布式缓存来保存机器ID和workId之间的映射关系。启动的时候访问分布式缓存查询当前机器ID对应的workId,如果查询不到则获取一个并保存到分布式缓存中。
    2.可通过Zookeeper管理workId,免去手动频繁修改集群节点,去配置机器ID的麻烦。

lastTimestamp上次生成ID的时间戳,这个是在内存中,系统时钟回退+重启后呢?无法保证

目前好像只能流程上控制系统时钟不回退。

41位 (timestamp - this.twepoch) << this.timestampLeftShift 超过长整型怎么办?

this.twepoch 可以设置当前开始使用系统时的时间,可以保证69年不超

Javascript 无法支持> 53位的数字怎么办?

js Number被表示为双精度浮点数,最大值为 Number.MAX_SAFE_INTEGER = 2^53-1
BigInt 是 JavaScript 中的一个新的原始数字类型,可以用任意精度表示整数。即使超出 Number 的安全整数范围限制,也可以安全地存储和操作大整数。
要创建一个 BigInt,将 n 作为后缀添加到任何整数文字字面量

BigInt 支持大数,那怎么控制这里用 64bits 长整型,左移溢出会出现问题吗?

这里不做处理会出现问题,BigInt 可以用任意精度表示整数
如何处理?暂不处理
此问题本质还是上面的41位时间差问题,69年不超,再长就超了,需要重新设计支持,也可以做溢出提示。
如果想限制为仅64位整数,则必须始终使用强制转换 BigInt.asIntN BigInt.asUintN
只要我们传递 BigInt 超过 64 位整数范围的值(例如,63 位数值 + 1 位符号位),就会发生溢出。

时钟回拨问题和解决方案讨论

首先看看时钟为什么会发生回拨?机器本地时钟可能会因为各种原因发生不准的情况,网络中提供了NTP服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题。

因为雪花算法强依赖机器时钟,所以难以避免受到时钟回拨的影响,有可能产生ID重复。原标准实现代码中是直接抛异常,短暂停止对外服务,这样在实际生产中是无法忍受的。所以要尽量避免时钟回拨带来的影响,解决思路有两个:

不依赖机器时钟驱动,就没时钟回拨的事儿了。即定义一个初始时间戳,在初始时间戳上自增,不跟随机器时钟增加。时间戳何时自增?当序列号增加到最大时,此时时间戳+1,这样完全不会浪费序列号,适合流量较大的场景,如果流量较小,可能出现时间断层滞后。
依然依赖机器时钟,如果时钟回拨范围较小,如几十毫秒,可以等到时间回到正常;如果流量不大,前几百毫秒或者几秒的序列号肯定有剩余,可以将前几百毫秒或者几秒的序列号缓存起来,如果发生时钟回拨,就从缓存中获取序列号自增。

参考链接:
https://github.com/cloudyan/snowflake

版权声明:程序员胖胖胖虎阿 发表于 2022年9月15日 上午10:24。
转载请注明:雪花id详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...