分布式id生成算法

雪花算法原理

雪花算法是一个分布式的ID生成算法,生成的ID是一个64bitLong类型的数字
在这里插入图片描述
64位组成部分 = 符号位(1bit) + 时间戳(41bit) + 机器ID(10bit) + 序列号(12bit)

符号位

第一部分是符号位, 因为是正数, 所以始终为0

时间戳

时间戳(Unix时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数, 不考虑闰秒)
第二部分是41bit长度的时间戳, 可以表示的范围是: 0(2^41 - 1), 也就是说41bit可以表示的最大值=(2^41 - 1)
将毫秒数转化成年 : (2^41 - 1) / (1L*1000*60*60*24*365) ≈ 69
注意: (2^41-1) = 2199023255551毫秒, 将其转换成时间是2039-09-07 23:47:35, 距离现在都不到20年, 为什么前面算出来的是69年?
我们会设置一个起始时间, 比如2022-01-01 00:00:00, 生成ID的时候, 计算当前时间戳与起始时间戳的差值, 这个差值最多能保存69年

机器ID

第三部分是10bit的机器ID, 取值范围为: 0(2^10-1), 即0-1023, 最多支持1024台机器

序列号

第四部分是12bit的序列ID, 取值范围为: 0到(2^12 - 1), 即0-4095, 就是同一毫秒内支持4096个id

优缺点

优点

  • 基于时间戳,同一时间戳下序列号自增,保证了有序性
  • 不依赖第三方库或者中间件,算法简单

缺点
依赖服务器时间,服务器时钟回拨时可能会生成重复 id

Java代码

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class MySnowFlake {
    /**
     * 起始的时间戳
     * 2022-10-10 08:00:00
     */
    private final static long START_TIMESTAMP = 1665360000000L;
    /**
     * 每一部分占用的位数
     */
    // 序列号占用的位数
    private final static long SEQUENCE_BIT = 12;
    // 机器ID占用的位数
    private final static long MACHINE_BIT = 10;

    /**
     * 每一部分的最大值
     */
    // 序列号最大值为 4095
    private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
    // 机器ID最大值为 1023
    private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);

    /**
     * 每一部分向左的位移
     */
    // 机器ID向左位移的位数 = 序列号的位数
    private final static long MACHINE_SHIFT = SEQUENCE_BIT;
    // 时间戳向左位移的位数 = 机器ID向左位移的位数 + 机器ID的位数
    private final static long TIMESTAMP_SHIFT = MACHINE_SHIFT + MACHINE_BIT;

    // 机器ID
    private long machineId;
    // 序列号
    private long sequence = 0L;
    // 上一次时间戳
    private long lastTimeStamp = -1L;

    /**
     * 根据机器ID生成指定的序列号
     * @param machineId    机器ID
     */
    public MySnowFlake(long machineId) {
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
        }
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     * 使用 synchronized 保证同一时刻只能有一个线程访问
     */
    public synchronized long nextId() {
        long currTimeStamp = getNewTimeStamp();
        if (currTimeStamp < lastTimeStamp) {
            // 出现时钟回拨
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
        if (currTimeStamp == lastTimeStamp) {
            // 相同毫秒内, 序列号自增, 这里相当于是取模操作
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currTimeStamp = getNextMill();
            }
        } else {
            // 不同毫秒内,序列号置为0
            sequence = 0L;
        }
        lastTimeStamp = currTimeStamp;
        /**
         * 这里作用主要是通过左移将数据移到对应的位置, 再通过 | 操作将数据拼接起来
         * | 操作 如果相对应位都是 0, 则结果为 0, 否则为 1
         */
        return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_SHIFT      // 时间戳部分
                | machineId << MACHINE_SHIFT                             // 机器标识部分
                | sequence;                                              // 序列号部分
    }

    /**
     * 当同一毫秒内的序列号达到最大时,使用while循环延迟时间使得 currTimeStamp > lastTimeStamp
     */
    private long getNextMill() {
        long mill = getNewTimeStamp();
        while (mill <= lastTimeStamp) {
            mill = getNewTimeStamp();
        }
        return mill;
    }

    /**
     * 获取当前时间戳
     */
    private long getNewTimeStamp() {
        return System.currentTimeMillis();
    }

    public static String formatByDateTimeMsPattern(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        return sdf.format(date);
    }

    public static String parseId(long id) {
        long _sequence = id & MAX_SEQUENCE;
        long _machineId = (id & (MAX_MACHINE_NUM << MACHINE_SHIFT)) >> MACHINE_SHIFT;
        long _time_stamp = id >>> (MACHINE_SHIFT + MACHINE_BIT) ;
        String thatTimeStr = formatByDateTimeMsPattern(new Date(START_TIMESTAMP + _time_stamp));
        return String.format("{\"ID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
                id, thatTimeStr, _machineId, _sequence);
    }

    public static void main(String[] args) {
        System.out.println(~(-1L << MACHINE_BIT));
        MySnowFlake snowFlake = new MySnowFlake( 1023);
        List<Long> idList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            idList.add(snowFlake.nextId());
        }
        idList.forEach(id -> {
            System.out.println(parseId(id));
        });
    }
}

UidGenerator

文档地址
UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。

依赖版本:Java8及以上版本, MySQL(内置WorkerID分配器, 启动阶段通过DB进行分配; 如自定义实现, 则DB非必选依赖)

Snowflake算法

在这里插入图片描述
Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。默认采用上图字节分配方式:

  • sign(1bit)
    固定1bit符号标识,即生成的UID为正数。

  • delta seconds (28 bits)
    当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年

  • worker id (22 bits)
    机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。

  • sequence (13 bits)
    每秒下的并发序列,13 bits可支持每秒8192个并发。

以上参数均可通过Spring进行自定义

CachedUidGenerator

RingBuffer环形数组,数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值,且为2^N。可通过boostPower配置进行扩容,以提高RingBuffer 读写吞吐量。
Tail指针、Cursor指针用于环形数组上读写slot:

  • Tail指针
    表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上curosr,此时可通过rejectedPutBufferHandler指定PutRejectPolicy

  • Cursor指针
    表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过rejectedTakeBufferHandler指定TakeRejectPolicy
    在这里插入图片描述

CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)

由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine 补齐方式。

在这里插入图片描述
RingBuffer填充时机

  • 初始化预填充
    RingBuffer初始化时,预先填充满整个RingBuffer.

  • 即时填充
    Take消费时,即时检查剩余可用slot量(tail - cursor),如小于设定阈值,则补全空闲slots。阈值可通过paddingFactor来进行配置,请参考Quick Start中CachedUidGenerator配置

  • 周期填充
    通过Schedule线程,定时补全空闲slots。可通过scheduleInterval配置,以应用定时填充功能,并指定Schedule时间间隔

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值