雪花算法SnowFlake

一、雪花算法是什么?

分布式中生成唯一性ID的一种算法 。

为啥不用数据库的自增主键呢?

  • 唯一性: 如果数据库数据特别多,你会同一张表建立不同节点上,数据也在不同节点上存,那么如果俩ID都是 001 违背主键 定义吗?
  • 顺序性: 雪花算法计算出来ID 有顺序 如果你了解数据库 B+树 ,对于索引来说 字段是 数字类型 ,有顺序, 唯一 在查找以及插入效率很高 而UUID是字符串没顺序不适合做数据库主键了

二、雪花算法的构成 

        雪花算法 由64位构成ID,对应java数据类型的话 long类型 这里位是二进制位 

  •         最左一位(图中没有标出)都是 0 因为二进制中 最左符号位 1代表负数 0是正数 而我们生成ID肯定是正数所以是0
  •         timestamp : 时间戳 我们一般情况给初始时间 用系统当前时间 减去 初始时间 这个差值的时间戳作为ID的时间戳也就是timestamp占用41位。至于为啥差值作为时间戳 1.减小时间戳长度 2.时钟回拨处理
  •        instance :这个表示机器个数 分布式系统中 多个节点 可以左边5位是机器的ID 右边5位 数据中心ID(机房ID)加起来是10位
  •        sequence:序列号 这个主要用途 并发执行代码 有时候获取时间一样的 那么区分ID 用序列号自增进行区分
  •         时钟回拨: 一种情况管理员手动把时间调整当前系统之前时间,这样的话生成ID和之前ID可能冲突了。虽然上边时间戳插值减轻该问题但是插值仍有可能为正值

 三、雪花算法实现思路

1、 如何把 时间戳,机器ID,序列号等合在一起变成一个long类型数字?

思路
  • 首先移动好说的 。数据中心ID datacenterID (属于instance) 假设 1L 号 用long表示可以 (二进制)
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
    将它左移动12位(序列号最长位)代码 datacenterID << 12 结果
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000
  • 机器ID 左移动 12位(序列号) + 5位(数据中心ID)= 17 workID << 17
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 0000 0000 0000 0000
  • 我们随便用时间戳差值 1110 0101 1010 0001 0001 10111 11 那么他图中右移12 + 5 + 5 = 22位
    0000 0000 0000 0000 1110 0101 1010 0001 0001 10111 1100 0000 0000 0000 0000 0000
  • 序列号最右边所以不用移动
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

接下来如何合并时间戳,机器ID,数据中心ID,序列号呢?
可以将两个long类型 按位或 进行合并 (按位或 只要有1 结果1 否则是0)
拿数据中心ID和机器ID举例子 

  • 数据ID 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000
  • 机器ID 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 0000 0000 0000 0000

进行或位运算( | )结果 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 0001 0000 0000 0000
这样进行合并了 

2、 如果并发访问 同一时间对于 要生成ID多于2的12次方个 也就是多余4096个ID如何处理?

思路

0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
该时间再生成ID就是
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000
对于这个我们构建一个类似于掩码数字 二进制表示
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
进行按位与 (&)两个都为1结果是1 否则为0
上边两个& 结果刚好 0 这个结果可以判断是否溢出
对于构建掩码那个数字 可以 -1 ^ (-1 << 12) ^ 是异或符号 两个二进制位 不同为1 相同为0
-1 的二进制如下
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

3、 发生时钟回拨如何处理?

思路
  • 可以记录上一次时间字段,如果当前时间 < 上一次记录时间 发送时钟回拨 抛异常提示
  • 可以进行等待,时间过了上一次记录时间进行ID生成
  • 也可以采用更高时间精度来生成时间戳

 四、代码实现

package util;
 
import java.util.Date;
 
/**
 * @ClassName: SnowFlakeUtil
 * @Author: jiaoxian
 * @Date: 2022/4/24 16:34
 * @Description:
 */
public class SnowFlakeUtil {
 
    private static SnowFlakeUtil snowFlakeUtil;
    static {
        snowFlakeUtil = new SnowFlakeUtil();
    }
 
    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    // 1650789964886:2022-04-24 16:45:59
    private static final long INIT_EPOCH = 1650789964886L;
 
    // 时间位取&
    private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;
 
    // 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
    private long lastTimeMillis = -1L;
 
    // dataCenterId占用的位数
    private static final long DATA_CENTER_ID_BITS = 5L;
 
    // dataCenterId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
 
    // dataCenterId
    private long dataCenterId;
 
    // workId占用的位数
    private static final long WORKER_ID_BITS = 5L;
 
    // workId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
 
    // workId
    private long workerId;
 
    // 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
    private static final long SEQUENCE_BITS = 12L;
 
    // 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
    // 0000000000000000000000000000000000000000000000000000111111111111
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
 
    // 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
    private long sequence;
 
    // workId位需要左移的位数 12
    private static final long WORK_ID_SHIFT = SEQUENCE_BITS;
 
    // dataCenterId位需要左移的位数 12+5
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
 
    // 时间戳需要左移的位数 12+5+5
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
 
    /**
     * 无参构造
     */
    public SnowFlakeUtil() {
        this(1, 1);
    }
 
    /**
     * 有参构造
     * @param dataCenterId
     * @param workerId
     */
    public SnowFlakeUtil(long dataCenterId, long workerId) {
        // 检查dataCenterId的合法值
        if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
            throw new IllegalArgumentException(
                    String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));
        }
        // 检查workId的合法值
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }
 
    /**
     * 获取唯一ID
     * @return
     */
    public static Long getSnowFlakeId() {
        return snowFlakeUtil.nextId();
    }
 
    /**
     * 通过雪花算法生成下一个id,注意这里使用synchronized同步
     * @return 唯一id
     */
    public synchronized long nextId() {
        long currentTimeMillis = System.currentTimeMillis();
        System.out.println(currentTimeMillis);
        // 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
        if (currentTimeMillis < lastTimeMillis) {
            throw new RuntimeException(
                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                            lastTimeMillis));
        }
        if (currentTimeMillis == lastTimeMillis) {
            // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
            // 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
            // 那么就使用新的时间戳
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimeMillis = getNextMillis(lastTimeMillis);
            }
        } else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
            sequence = 0;
        }
        // 记录最后一次使用的毫秒时间戳
        lastTimeMillis = currentTimeMillis;
        // 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
        // <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
        // |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
        // 优先级:<< > |
        return
                // 时间戳部分
                ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
                // 数据中心部分
                | (dataCenterId << DATA_CENTER_ID_SHIFT)
                // 机器表示部分
                | (workerId << WORK_ID_SHIFT)
                // 序列号部分
                | sequence;
    }
 
    /**
     * 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
     * @param lastTimeMillis 指定毫秒时间戳
     * @return 时间戳
     */
    private long getNextMillis(long lastTimeMillis) {
        long currentTimeMillis = System.currentTimeMillis();
        while (currentTimeMillis <= lastTimeMillis) {
            currentTimeMillis = System.currentTimeMillis();
        }
        return currentTimeMillis;
    }
 
    /**
     * 获取随机字符串,length=13
     * @return
     */
    public static String getRandomStr() {
        return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);
    }
 
    /**
     * 从ID中获取时间
     * @param id 由此类生成的ID
     * @return
     */
    public static Date getTimeBySnowFlakeId(long id) {
        return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);
    }
 
    public static void main(String[] args) {
        SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();
        long id = snowFlakeUtil.nextId();
        System.out.println(id);
        Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);
        System.out.println(date);
        long time = date.getTime();
        System.out.println(time);
        System.out.println(getRandomStr());
 
    }
 
}

五、算法优缺点

1、雪花算法有以下几个优点:

高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
不依赖第三方库或者中间件。
算法简单,在内存中进行,效率高。

2、雪花算法有如下缺点:

        依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id

六、注意事项 

        其实雪花算法每一部分占用的比特位数量并不是固定死的。例如你的业务可能达不到 69 年之久,那么可用减少时间戳占用的位数,雪花算法服务需要部署的节点超过1024 台,那么可将减少的位数补充给机器码用。

        注意,雪花算法中 41 位比特位不是直接用来存储当前服务器毫秒时间戳的,而是需要当前服务器时间戳减去某一个初始时间戳值,一般可以使用服务上线时间作为初始时间戳值。

        对于机器码,可根据自身情况做调整,例如机房号,服务器号,业务号,机器 IP 等都是可使用的。对于部署的不同雪花算法服务中,最后计算出来的机器码能区分开来即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值