雪花算法生成分布式ID源码分析及低频场景下全是偶数的解决办法

目录

雪花算法原理介绍

雪花算法源码分析

低频场景下都是偶数的原因

解决雪花算法的偶数问题

1、切换毫秒时使用随机数

2、抖动上限值加抖动序列号


雪花算法原理介绍

雪花算法(snowflake)最早是twitter内部使用的分布式下的唯一id生成算法,在2014年开源,开源的版本由scala编写,地址为https://github.com/twitter-archive/snowflake,该算法具有以下特性

  • 唯一性:高并发分布式系统中生成id唯一
  • 高性能:每秒可生成百万个id
  • 有序性:生成的id是有序递增的​
  • 不依赖第三方的库或者中间件

        算法产生的是一个Long型 64 bit位的值,转换成字符串长度最长19位。

SnowFlake的结构(每部分用-分开):

0-00000000000000000000000000000000000000000-00000-00000-000000000000

  1. 第一部分:1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,ID要是正数,最高位是0
  2. 第二部分:41位毫秒数,不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 自定义起始时间截),自定义起始时间戳即人为指定的算法起始时间,当前时间即生成ID时的时间戳41位的时间截,可以使用约69年, (1L << 41) / (365 * 24 * 3600 * 1000)≈ 69
  3. 第三、四部分:10位的数据机器位,可以部署在1024(1L<<10)个节点,包括5位datacenterId(机房)和5位workerId(机器号)
  4. 第五部分:12位序列,每毫秒可生成序列号数,共4096(1L<<12)个ID序号

以上5部分总64bit,即需要一个Long整型来记录

SnowFlake的优点:

  1. 整体按时间自增排序
  2. Long整型ID,存储高效,检索高效
  3. 分布式系统内无ID碰撞(各分区由datacenterId和workerId来区分)
  4. 生成效率高,占用系统资源少,理论每秒可生成1000 * 4096 = 4096000个,也就是单台机器理论上每秒钟可以生成400万个分布式ID序列号

雪花算法源码分析

        雪花算法源码如下所示,代码里面都做了详细的说明

public class SnowFlake {

    /**
     * 数据中心/机房标识所占bit位数
     */
    private final static long DATACENTER_BIT = 5;
    /**
     * 机器标识所占bit位数
     */
    private final static long WORKER_ID_BIT = 5;
    /**
     * 每毫秒下的序列号所占bit位数,2的12次方,0到4095,一秒钟最多生产4096个数字
     */
    private final static long SEQUENCE_BIT = 12;

    // 起始时间戳 2023-07-14 00:00:00,可以根据自己的需求进行修改
    private final static long EPOCH=1689264000000L;
    // 机器标志相对序列号的偏移量 12位
    private final static long WORKER_ID_LEFT = SEQUENCE_BIT;
    // 机房标志相对机器的偏移量 17 = 12 + 5位
    private final static long DATACENTER_LEFT = WORKER_ID_LEFT + WORKER_ID_BIT;
    // 时间戳标志相对机器的偏移量 22 = 17 + 5位
    private final static long TIMESTAMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    // 用位运算计算出最大支持的数据中心编号 31
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    // 用位运算计算出最大支持的机器编号 31
    private final static long MAX_WORKER_ID_NUM = -1L ^ (-1L << WORKER_ID_BIT);
    // 用位运算计算出12位能存储的最大整数,12位的情况下为4095
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    /*
    机房 机器 序列号 上一次请求保存的时间戳
     */
    private long datacenterId;
    private long workerIdId;
    private long sequence = 0L;// 自增序列号(相当于计数器)
    private long lastStamp = -1L;

    public SnowFlake() {}
    
    /**
     * 构造函数
     *
     * @param workerId     机器ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public  SnowFlake(long workerId, long datacenterId) {
        if (workerId > MAX_WORKER_ID_NUM || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID_NUM));
        }
        if (datacenterId > MAX_DATACENTER_NUM  || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_NUM));
        }
        this.workerIdId = workerId;
        this.datacenterId = datacenterId;
    }
    
    /**
     * 产出下一个Id
     * @return
     */
    public synchronized long nextId() {
        // 获取当前的时间戳
        long curStamp = getCurrentStamp();
        // 若当前时间戳 < 上次时间戳则抛出异常
        if (curStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwords. Refusing to generate id");
        }
        // 1.同一毫秒内
        if (curStamp == lastStamp) {
            // 1.1 相同毫秒内 id自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 1.2 同一毫秒内 序列数已经达到最大4095,等待下一个毫秒到来在生成
            if (sequence == 0L) {
                // 获取下一秒的时间戳并赋值给当前时间戳
                curStamp = getNextMill();
            }
        } else {
            // 2.不同毫秒,序列号重置为0
            sequence = 0L;
        }
        // 3.当前时间戳存档, 用于下次生成id对比是否为同一毫秒内
        lastStamp = curStamp;
        // 4.或运算拼接返回id
        return (curStamp - EPOCH) << TIMESTAMP_LEFT // 时间戳部分
                | datacenterId << DATACENTER_LEFT // 机房部分
                | workerIdId << WORKER_ID_LEFT // 机器部分
                | sequence; // 序列号部分
    }

    private long getNextMill() {
        long mill = getCurrentStamp();
        // 循环获取当前时间戳, 直到拿到下一秒的时间戳
        while (mill <= lastStamp) {
            mill = getCurrentStamp();
        }
        return mill;
    }

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

    public static void main(String[] args) throws InterruptedException {
        SnowFlake snowFlake = new SnowFlake();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1L);
            System.out.println(snowFlake.nextId());
        }
    }
}

        我们运行测试一下,注意测试中每次都调用都休眠了一毫秒,来模拟低频次调用生成分布式ID的场景,运行后控制台输出如下所示:可以看到生成的都是偶数

214341257265152
214341261459456
214341269848064
214341278236672
214341286625280
214341290819584
214341295013888
214341303402496
214341307596800
214341315985408

低频场景下都是偶数的原因

        低频场景下调用时,每次都是不同毫秒值的时间戳,导致每次都走到了sequence = 0L,即序列号都是0,最终生成序列号时是通过|或运算生成的,最后一位都是0或运算得到的还是0,转成Long整数时就都是偶数了

public synchronized long nextId() {
       ......
        // 1.同一毫秒内
        if (curStamp == lastStamp) {
           ......
        } else {
            // 2.不同毫秒,序列号重置为0
            sequence = 0L;
        }
        ......
    }

        如果生成的序列号都是偶数,并且作为分库分表的分片键时就会出现严重的数据倾斜问题,这个问题是非常严重的(解决办法可以将该序列号计算hashcode再进行分片计算)

解决雪花算法的偶数问题

1、切换毫秒时使用随机数

        改动代码主要是sequence = ThreadLocalRandom.current().nextLong(randomSequenceLimit)这一行,就是在跨毫秒时自增计数器不再初始化为0,而是取一个0或者1的随机数,通过这种形式来避免都是偶数的问题。

/**
 * 此属性用于限定一个随机上限,在不同毫秒下生成序号时,给定一个随机数,避免偶数问题。
 * 注意次数必须小于{@link #MAX_SEQUENCE}
 * 不同毫秒,序列号取一个[0,randomSequenceLimit)之间的随机数,避免都是偶数的情况
 */
private final long randomSequenceLimit=2;

public synchronized long nextId() {
	......
	if (curStamp == lastStamp) {
		......
	} else {
		// 2.不同毫秒,序列号取一个[0,randomSequenceLimit)之间的随机数,避免都是偶数的情况
		sequence = ThreadLocalRandom.current().nextLong(randomSequenceLimit);
        //sequence = 0L;
	}
	......
}

2、抖动上限值加抖动序列号

  • maxVibrationOffset:最大抖动上限值,即在跨毫秒时如果sequenceOffset的值超过了maxVibrationOffset则归0,最好设置为奇数,注意该值必须小于等于MAX_SEQUENCE即4095
  • sequenceOffset:跨毫秒时的序列号,不同毫秒,超过了抖动上限则将sequenceOffset计数器归0,否则sequenceOffset累加1,sequence的值设置为sequenceOffset

        通过sequenceOffset序列号自增保证了跨毫秒时不会一直出现偶数的情况,通过maxVibrationOffset的限制保证sequenceOffset不会无限递增

//最大抖动上限值,最好设置为奇数,注意该值必须小于等于MAX_SEQUENCE即4095
private int maxVibrationOffset=1;
//跨毫秒时的序列号,跨毫秒获取时该序列号+1
private volatile int sequenceOffset = -1;

public synchronized long nextId() {
	......
	if (curStamp == lastStamp) {
		......
	} else {
		// 2.不同毫秒,处理抖动上限,超过了抖动上限则将sequenceOffset计数器归0,否则sequenceOffset累加1
        //将sequence设置为sequenceOffset
		vibrateSequenceOffset();
        sequence = sequenceOffset;
        //sequence = 0L;
	}
	......
}

private void vibrateSequenceOffset() {
    //不同毫秒时间,处理抖动上限,超过了抖动上限则将sequenceOffset计数器归0,否则sequenceOffset累加1
    sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
}

通过测试程序进行验证:

第一种通过随机数的方式,基本上解决了跨毫秒的偶数问题,但是因为随机数的缘故,跨毫秒的奇偶数不能保证百分百的1:1,不过也可以接受;

第二种将maxVibrationOffset设置为奇数时,可以在跨毫秒时保证生成的奇偶数序列号数量为1:1,比第一种随机数的效果要好一些。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
雪花算法是种生成分布式ID的算法,它可以生成一个64位的ID,其中包含了时间戳、数据中心ID和机器ID等信息。下面是雪花算法生成分布式ID的软件设计模型: 1. 定义一个Snowflake类,该类包含以下属性: - datacenter_id: 数据中心ID,占5位,取值围为0~31。 - worker_id: 机器ID,占5位,取值范围为0~31。 - sequence: 序列号,占12位,取值范围为0~4095。 - last_timestamp: 上一次生成ID的时间戳。 2. 实现Snowflake类的构造函数,初始化datacenter_id和worker_id属性。 3. 实现一个next_id方法,该方法用于生成下一个ID。具体实现如下: - 获取当前时间戳,单位为毫秒。 - 如果当前时间戳小于上一次生成ID的时间戳,则说明系统时钟回退过,抛出异常。 - 如果当前时间戳等于上一次生成ID的时间戳,则将序列号加1。 - 如果当前时间戳大于上一次生成ID的时间戳,则将序列号重置为0,并将last_timestamp属性更新为当前时间戳。 - 将datacenter_id、worker_id、时间戳和序列号按照一定的位数组合成一个64位的ID。 - 返回生成ID。 4. 在分布式系统中,每个节点都需要创建一个Snowflake实例,并指定不同的datacenter_id和worker_id。每个节点生成ID都是唯一的,且具有时间顺序。 下面是一个Python实现的雪花算法生成分布式ID的代码示例: ```python import time class Snowflake: def __init__(self, datacenter_id, worker_id): self.datacenter_id = datacenter_id self.worker_id = worker_id self.sequence = 0 self.last_timestamp = -1 def next_id(self): timestamp = int(time.time() * 1000) if timestamp < self.last_timestamp: raise Exception("Clock moved backwards. Refusing to generate id") if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 if self.sequence == 0: timestamp = self.wait_next_millis(self.last_timestamp) else: self.sequence = 0 self.last_timestamp = timestamp return ((timestamp - 1288834974657) << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence def wait_next_millis(self, last_timestamp): timestamp = int(time.time() * 1000) while timestamp <= last_timestamp: timestamp = int(time.time() * 1000) return timestamp ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值