分布式Id生成之雪花算法(SnowFlake)

news2024/11/20 9:24:59

目录

前言

回顾二进制

二进制概念

运算法则

位(Bit)

字节(Byte)

字符

字符集

二进制原码、反码、补码

有符号数和无符号数

疑问:为什么不是-127 ~ 127 ?

为什么需要分布式全局唯一ID以及分布式ID得业务需求?

ID生成规则部分硬性要求

ID生成系统的可用性要求

通用解决方案

1. UUID

优点

缺点

2. 数据库自增主键

优点

缺点

3. 基于Redis生成全局ID策略

单机版

集群分布式

缺点

SnowFlake雪花算法

雪花算法结构

雪花算法的优缺点

优点

缺点

雪花算法的具体实现

SpringtBoot整合雪花算法 (基于hutool工具类)

第三方基于SnowFlake算法生成的唯一ID

雪花算法相关问题以及解决方案

1. 时间回拨问题

2.工作机器ID可能会重复的问题

3. -1L ^ (-1L << x) 表示什么?

4.前端直接使用发生精度丢失


前言

        本文章首先回顾一下二进制相关知识点,方便猿友们阅读的更清晰,不至于读完还是一脸懵的状态;其次,现阶段为什么需要分布式全局唯一ID以及分布式ID的业务需求,进行详细描述一下;以及此问题提出后一般通用的解决方案和优缺点;最后详细描述一下本文章的核心功能点——雪花算法(SnowFlake)。

        吾在写本文章时也是处于不是太懂具体实现以及优劣势,才下决定好好研究一波,希望能够帮助到还不明白的猿友们,若本文有不足之处,欢迎大佬点评!!!

回顾二进制

二进制概念

二进制是计算技术中广泛采用的一种数制。二进制数据是用01两个数码来表示的数。

它的基数位2,进位规则是“逢二进一”,借位规则是“借一当二”,由18世纪德国数理哲学大师莱布尼兹发现。

当前的计算机系统使用的基本上是二进制系统,数据在计算机中主要是以补码的形式存储的。

计算机中的二进制则是一个非常微小的开关,用“开”来表示1,“关”来表示0。

运算法则

二进制的运算算术运算

二进制的加法(+):0+0=0,0+1=1,1+0=1,1+1=10(向高位进位);例:7=111;10=1010;3=11

二进制的减法(-):0-0=0,0-1=1(先高位错位)1-0=1,1-1=0(模二加运算或异或运算);

二进制的乘法(*):0*0=0;0*1=0;1*0=0;1*1=1;

二进制的除法(/):0/0=0,0/1=0,1/0=0(无意义),1/1=1;

逻辑运算二进制的或(|)运算:遇1得1

逻辑运算二进制的与(&)运算:遇0得0

逻辑运算二进制得非(!)运算:各位取反。

位(Bit)

数据存储得最小单位。每个二进制数字0或者1就是1个位。

字节(Byte)

8个位构成一个字节;即1byte(字节)= 8bit(位)

  • 1KB = 1024B(字节);
  • 1MB = 1024KB;(2^10 B)
  • 1GB = 1024MB;(2^20 B)
  • 1TB  = 1024GB; (2^30 B)

字符

a、A、中、+、*、@……均表示一个字符;

        一般utf-8编码下,一个汉字 字符 占用3个字节;

        一般gbk编码下,一个汉字 字符 占用2个字节。

字符集

即各种各个字符的集合,也就是说哪些汉字,字母(A、b、c)和符号(空格、引号..)会被收入标准中

二进制原码、反码、补码

正数负数
原码原码原码
反码原码原码符号位外按位取反
补码原码反码+1

有符号数和无符号数

无符号数中,所有的位都用于直接表示该值的大小。

有符号数中,最高位用于表示正负。

例:

         8位2进制表示的:

                无符号数的范围为0(00000000B) ~ 255 (11111111B);

                有符号数的范围为-128(10000000B) ~ 127 (01111111B);

                255 = 1*2^7 + 1*2^6 + 1*2^5 +1*2^4 +1*2^3 +1*2^2 +1*2^1 +1*2^0;

                127 = 1*2^6 + 1*2^5 +1*2^4 +1*2^3 +1*2^2 +1*2^1 +1*2^0;

疑问:为什么不是-127 ~ 127 ?

补码比其它码多一位,这是为什么呢?问题出在0上。

[+0]原码=0000 0000, [-0]原码=1000 0000

[+0]反码=0000 0000, [-0]反码=1111 1111

[+0]补码=0000 0000, [-0]补码=0000 0000

反码表示法规定:正数的反码与其原码相同。负数的反码是对其原码逐位取反,但符号位除外。

在规定中,8位二进制码能表示的反码范围是-127~127。

-128没有反码。

为什么规定-128没有反码呢?

首先看-0,[-0]原码=1000 000,其中1是符号位,根据反码规定,算出[-0]反码=1111 1111, 再看-128,[-128]原码=1000 000,假如让-128也有反码,根据反码规定,则[-128]反码=1111 1111,

-128的反码和-0的反码相同,所以为了避免面混淆,有了-0,便不能有-128这是反码规则决定的

因此八位二进制表示的范围为:-128~0~127。此处的0为正0,负0表示了-128。

为什么需要分布式全局唯一ID以及分布式ID得业务需求?

在复杂分布式系统中,往往需要对大量得数据和消息进行唯一标识,如美团点评得金融、支付、餐饮、酒店、订单、优惠券都需要由唯一ID做标识

ID生成规则部分硬性要求

  • 全局唯一
  • 趋势递增
    • 在MySQL得InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据结构来存储索引,在主键的选择上面应该尽量使用有序的主键保证写入性能
  • 单调递增
    • 保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求
  • 信息安全
    • 如果ID是连续,恶意用户的爬取工作就非常容易做了,直接按照顺序下载指定URL即可,如果是订单号就危险了,竞争对手可以直接知道我们一天的单量,所以在一些应用场景下,需要ID无规则不规则。
  • 含时间戳
    • 一样能够快速在开发中了解这个分布式ID什么时候生成的

ID生成系统的可用性要求

  • 高可用
    • 发布一个获取分布式ID请求,服务器就要保证99.999%的情况下给我创建一个唯一分布式ID
  • 低延迟
    • 发一个获取分布式ID的请求,服务器就要快,极速
  • 高QPS
    • (每秒查询率(QPS,Queries-per-second)是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准)。
      • 例如并发一口气10万个创建分布式ID请求同时杀过来,服务器要顶得住且一下子成功创建10万个分布式ID

通用解决方案

1. UUID

UUID.randomUUID(),UUID的标准型包含32个16进制数字,以连字号分为五段,形式位 8-4-4-4-12的36个字符,性能非常高,本地生成,没有网络消耗。

优点

  • 能够保证独立性,程序可以在不同的数据库间迁移,效果不受影响。
  • 保证生成的ID不仅是表独立的,而且是库独立的,这点在你想切分数据库的时候尤为重要。

缺点

  • 入数据库性能差,因为UUID是无序的

        无序,无法预测他的生成顺序,不能生成递增有序的数字

        首先分布式id一般都会作为主键,但是按照mysql官方推荐主键尽量越短越好,UUID每一个都很长,所以不是很推荐。

  • 主键,ID最为主键时,在特定的环境下会存在一些问题

        比如做DB主键的场景下,UUID就非常不适用MySQL官方明确的说明

  • 索引,B+树索引的分裂

        既然分布式ID是主键,然后主键是包含索引的,而mysql的索引是通过B+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的B+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键的B+树进行很大的修改,这一点很不好,插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。

UUID只能保证全局唯一性,不满足后买你的趋势递增,单调递增

2. 数据库自增主键

在分布式里面,数据库的自增ID机制的主要原理是:数据库自增ID和mysql数据库的replace into实现的,这里的replace intoinsert功能类似,不同点在于:replace into首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主机那或者唯一索引判断)则先删除,在插入,否则直接插入新数据。

REPLCAE INTO的含义是插入一条记录,如果表中唯一索引的值遇到冲突,则替换老数据

create table t_test(
    id bigint(20) unsigned not null auto_increment primary key,
    stub char(1) not null default '',
    unique key stub (stub)
)
REPLACE into t_test(stub) values('b');
select LAST_INSERT_ID();

每次插入的时候,发现都会把原来的数据给替换,并且ID也会增加。

满足了:递增性、单调性、唯一性

优点

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。

缺点

  • 不适合做分布式ID,系统水平扩展比较困难

        比如定义好步长和机器台数之后,如果要添加机器该怎么办,假设现在有一台机器发号是:1,2,3,4(步长是1),这个时候需要扩容机器一台,可以这样做:把第二台机器的初始值设置得比第一台超过很多,貌似还好,但是假设线上如果有100台机器,这个时候扩容要怎么做,简直是噩梦,所以系统水平扩展方案复杂难以实现。

  • 数据库压力大

        每次获取ID都得读写一次数据库,非常影响性能,不符合分布式ID里面得延迟低和高QPS得规则(在高并发下,如果都去数据库里面获取ID,那是非常影响性能的)

  • 数据库迁移以及分表分库困难
    • 不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
    • 在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。
    • 如果涉及多个系统需要合并或者数据迁移比较麻烦。
    • 分表分库的时候麻烦。

3. 基于Redis生成全局ID策略

单机版

因为Redis是单线程,天生保证原子性,可以使用原子操作INCRINCEBY来实现、

  • INCRBY:设置增长步长。key是redis中的键,将key所存储的值加上增量integer。如果key不存在,那么key的值就会被初始化为0,然后在执行incrby命令。
  • INCR:key是redis中的键,只是每次都是默认自增1。

集群分布式

        注意:在Redis集群情况下,同样和MySQL一样需要设置不同的增长步长,同时key一定要设置有效期,可以使用Redis集群来获取更高的吞吐量。

假设一个集群中有5台Redis,可以初始化每台Redis的值分别是1,2,3,4,5,然后设置步长都是5

各个Redis生成的ID为:

A:1 6 11 16 21
B:2 7 12 17 22
C:3 8 13 18 23
D:4 9 14 19 24
E:5 10 15 20 25

缺点

  • Redis集群的维护和保养比较麻烦,配置麻烦。
  • 要设置单点故障,哨兵值守。

SnowFlake雪花算法

SnowFlake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。

核心组成:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit是机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),最后还有一个符号位,永远是0. 

核心思想:分布式、唯一。

雪花算法结构

 结构格式(64bit):1bit保留 + 41bit时间戳 + 10bit机器 + 12bit序列号

<1> 1bit-保留不用

        因为二进制中最高位是符号位,1标识负数,0标识正数,生成的id一般都是用整数,所以最高位固定为0.

<2>  41bit-用来记录时间戳(毫秒)

        41位可以表示2^41-1个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0至2^41-1,减1是因为可表示的数值范围是从0开始算的,而不是1.也就是说41位可以表示2^41-1个毫秒的值,转化成单位年则是:

 (2^41−1)/(1000∗60∗60∗24∗365)=69年 ,也就是说这个时间戳可以使用69年不重复

注意:其时间戳的算法是1970年1月1日到指点时间所经过的毫秒或秒数,那咱们把开始时间从2021年开始,就可以延长41位时间戳能表达的最大时间,所以这里实际指的是相对自定义开始时间的时间戳。

<3> 10bit-用来记录工作机器id

  • 可以部署在2^10=1024个节点,包括5位datacenterId和5位workerId
  • 5位(bit)可以表示的最大正整数是2^5-1=31,即可以用0、1、2、3、……31这32个数字,来表示不同的datecenterId或者workerId

<4> 12bit-序列号,用来记录同毫秒内产生的不同id

  • 12位(bit)可以表示的最大正整数是2^12-1=4095,即可以用0、1、2、3、……4095这4096个数字,来表示同一机器同一时间戳(毫秒)内产生的4096个ID序号
  • SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID?

        同一毫秒的ID数量 = 1024 * 4096 = 4194304,所以最大可以支持单应用差不多四百万的并发量。

注意:上面总体是64位,具体位数可自行配置,

        若想运行更久,需要增加时间戳位数;

        若想支持更多节点,可增加工作机器Id位数;

        若想支持更高并发,增加序列号位数。

雪花算法的优缺点

优点

  • 系统环境ID不重复

        能满足高并发分布式系统环境ID不重复,比如大家熟知的分布式场景下的数据库表的ID生成。

  • 生成效率极高

        在高并发,以及分布式环境下,除了生成不重复id,每秒可生成百万个不重复id,生成效率极高。

  • 保证基本有序递增

        基于时间戳,可以保证基本有序递增,很多业务场景都有这个需求。

  • 不依赖第三方库

        不依赖第三方的库,或者中间件,算法简单,在内存中进行。

缺点

  • 强依赖系统时钟,如果主机时间回拨,则会造成重复ID
  • 在单机上是递增的,但由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况,此缺点可以认为无所谓,一般分布式ID只要求趋势递增,并不会严格要求递增,90%的需求只要求趋势递增。

雪花算法的具体实现

注意!注意!注意!以下代码亲测过,并有详细的注释解说,直接拿走,不谢!!!

public class SnowFlakeUtil {

    /**
     * 初始时间戳,可以根据业务需求更改时间戳
     */
    private final long twepoch = 11681452025134L;

    /**
     * 机器ID所占位数,长度为5位
     */
    private final long workerIdBits = 5L;

    /**
     * 数据标识ID所占位数,长度位5位
     */
    private final long datacenterIdBits = 5L;

    /**
     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /**
     * 支持的最大数据标识id,结果是31
     */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /**
     * 序列在id中占的位数
     */
    private final long sequenceBits = 12L;

    /**
     * 工作机器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 数据标识id向左移17位(12+5)
     */
    private final long dataCenterIdShift = sequenceBits + workerIdBits;

    /**
     * 时间截向左移22位(5+5+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /**
     * 序列号最大值; 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * 工作机器ID(0~31),2进制5位  32位减掉1位 31个
     */
    private volatile long workerId;

    /**
     * 数据中心ID(0~31),2进制5位  32位减掉1位 31个
     */
    private volatile long datacenterId;

    /**
     * 毫秒内序列(0~4095),2进制12位 4096 - 1 = 4095个
     */
    private volatile long sequence = 0L;

    /**
     * 上次时间戳,初始值为负数
     */
    private volatile long lastTimestamp = -1L;


    // ==============================Constructors=====================================

    /**
     * 有参构造
     * @param workerId 工作机器ID(0~31)
     * @param datacenterId 数据中心ID(0~31)
     * @param sequence 毫秒内序列(0~4095)
     */
    public SnowFlakeUtil(long workerId, long datacenterId, long sequence){
        // sanity check for workerId
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    // ==============================Methods==========================================

    /**
     * 获得下一个ID (该方法是线程安全的)
     *  如果一个线程反复获取Synchronized锁,那么synchronized锁将变成偏向锁。
     * @return 生成的ID
     */
    public synchronized long nextId() {
        // 获取当前时间的时间戳,单位(毫秒)
        long timestamp = timeGen();

        // 获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
        if (timestamp < lastTimestamp) {
            System.err.printf("当前时间戳不能小于上次时间戳,上次时间戳为: %d.", lastTimestamp);
            throw new RuntimeException(String.format("当前时间戳不能小于上次时间戳,生成ID失败. 时间戳差值: %d milliseconds",
                    lastTimestamp - timestamp));
        }

        // 获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
        if (lastTimestamp == timestamp) {
            /* 逻辑:意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
                    这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围 */
            // sequence:毫秒内序列(0~4095);  sequenceMask: 序列号最大值;
            sequence = (sequence + 1) & sequenceMask;
            /* 逻辑:当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID */
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        // 将上次时间戳值刷新(逻辑:记录一下最近一次生成id的时间戳,单位是毫秒)
        lastTimestamp = timestamp;

        /* 核心逻辑:生成一个64bit的id;
                  先将当前时间戳左移,放到41 bit那儿;
                  将机房id左移放到5 bit那儿;
                  将机器id左移放到5 bit那儿;
                  将序号放最后12 bit
                  最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型 */
        /*
         * 返回结果:
         * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
         * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
         * (workerId << workerIdShift) 表示将工作id左移相应位数
         * | 是按位或运算符,例如:x | y,只有当x,y不为0的时候结果才为0,其它情况结果都为1。
         * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
         */
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << dataCenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    /**
     * 上次时间戳与当前时间戳进行比较
     * 逻辑:当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
     * @param lastTimestamp 上次时间戳
     * @return 若当前时间戳小于等于上次时间戳(时间回拨了),则返回最新当前时间戳; 否则,返回当前时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 获取系统时间戳
     * @return 当前时间的时间戳 14位
     */
    private long timeGen(){
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil(1,1,0);
        System.out.println(snowFlakeUtil.timeGen());
        for (int i = 0; i < 100; i++) {
            System.out.println("雪花算法生成第【"+(i+1)+"】个ID:"+ snowFlakeUtil.nextId());
        }

    }

}

以上代码执行结果:

雪花算法生成第【1】个ID:-5049534853385416704
雪花算法生成第【2】个ID:-5049534853385416703
雪花算法生成第【3】个ID:-5049534853385416702
雪花算法生成第【4】个ID:-5049534853385416701
雪花算法生成第【5】个ID:-5049534853385416700
雪花算法生成第【6】个ID:-5049534853385416699
雪花算法生成第【7】个ID:-5049534853385416698
雪花算法生成第【8】个ID:-5049534853385416697
雪花算法生成第【9】个ID:-5049534853385416696
雪花算法生成第【10】个ID:-5049534853385416695
……
……
…… (之后的结果就不一一展示了)

SpringtBoot整合雪花算法 (基于hutool工具类)

第一步:引入hutool工具依赖包

<!-- hutool工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.1</version>
</dependency>

第二步:Springboot整合具体代码实现

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;

import javax.annotation.PostConstruct;

/**
 * SpringtBoot整合雪花算法 (基于hutool工具类)
 */
public class SnowFlakeHutoolTestController {
    /**
     * 工作机器ID(0~31),2进制5位  32位减掉1位 31个
     */
    private long workerId = 0;
    /**
     * 数据中心ID(0~31),2进制5位  32位减掉1位 31个
     */
    private long datacenterId = 1;

    /**
     * 雪花算法对象
     */
    private Snowflake snowFlake = IdUtil.createSnowflake(workerId, datacenterId);

    @PostConstruct
    public void init() {
        try {
            // 将网络ip转换成long
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取雪花ID,默认使用网络IP作为工作机器ID
     * @return ID
     */
    public synchronized long snowflakeId() {
        return this.snowFlake.nextId();
    }

    /**
     * 获取雪花ID
     * @param workerId 工作机器ID
     * @param datacenterId 数据中心ID
     * @return ID
     */
    public synchronized long snowflakeId(long workerId, long datacenterId) {
        Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
        return snowflake.nextId();
    }

    public static void main(String[] args) {
        SnowFlakeHutoolTestController snowFlakeDemo = new SnowFlakeHutoolTestController();
        for (int i = 0; i < 20; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println("雪花算法生成第【"+(finalI +1)+"】个ID:"+ snowFlakeDemo.snowflakeId());
            }, String.valueOf(i)).start();
        }
    }

}

以上代码执行结果:

雪花算法生成第【2】个ID:1646777064113700865
雪花算法生成第【5】个ID:1646777064113700868
雪花算法生成第【3】个ID:1646777064113700866
雪花算法生成第【4】个ID:1646777064113700867
雪花算法生成第【1】个ID:1646777064113700864
雪花算法生成第【11】个ID:1646777064113700873
雪花算法生成第【10】个ID:1646777064113700872
雪花算法生成第【8】个ID:1646777064113700871
雪花算法生成第【7】个ID:1646777064113700870
雪花算法生成第【6】个ID:1646777064113700869
雪花算法生成第【16】个ID:1646777064113700877
雪花算法生成第【13】个ID:1646777064113700876
雪花算法生成第【12】个ID:1646777064113700875
雪花算法生成第【9】个ID:1646777064113700874
雪花算法生成第【17】个ID:1646777064113700881
雪花算法生成第【19】个ID:1646777064113700880
雪花算法生成第【15】个ID:1646777064113700879
雪花算法生成第【14】个ID:1646777064113700878
雪花算法生成第【20】个ID:1646777064113700883
雪花算法生成第【18】个ID:1646777064113700882

第三方基于SnowFlake算法生成的唯一ID

  • 百度UIDGenerator:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
    • UidGenerator是Java实现的,基于SnowFlake算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中,支持自定义workerId位数和初始化策略,从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。
  • 美团Leaf:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html
    • leaf-segment 方案
      • 优化:双buffer + 预分配
      • 容灾:Mysql DB 一主两从,异地机房,半同步方式
      • 缺点:如果用segment号段式方案:id是递增,可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。
  • leaf-snowflake方案
    • 使用Zookeeper持久顺序节点的特性自动对snowflake节点配置workerID
      • 1.启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
      • 2.如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
      • 3.如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

雪花算法相关问题以及解决方案

1. 时间回拨问题

问题

        在获取时间的时候,可能会出现时间回拨的问题,什么是时间回拨呢?

原因

        时间回拨就是服务器上的时间突然倒退到之前的时间。

  • 认为原因,把系统环境的时间改了
  • 有时候不同的机器上需要同步时间,可能不同机器之间存在误差,那么可能会出现时间回拨问题

解决方案

  • 回拨时间小的时候,不生成ID,循环等待到时间点到达。(只适合时钟回拨较小的)
  • 利用拓展位,回拨之后再扩展位上加1就可以了,这样ID依然可以保持唯一。但是找个要求我们提前预留出位数,要么从机器id中,要么从序列号中,腾出一定的位,在时间回拨的时候,这个位置+1.
  • 缓存workerID,减少第三方组件的依赖
  • 由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警

2.工作机器ID可能会重复的问题

机器 ID(5 位)和数据中心 ID(5 位)配置没有解决(不一定各是5位,可自行配置),分布式部署的时候会使用相同的配置,仍然有 ID 重复的风险。

解决方案

  • workId 使用服务器 hostName 生成,dataCenterId 使用 IP 生成,这样可以最大限度防止 10 位机器码重复,但是由于两个 ID 都不能超过 32,只能取余数,还是难免产生重复,但是实际使用中,hostName 和 IP 的配置一般连续或相近,只要不是刚好相隔 32 位,就不会有问题,况且,hostName 和 IP 同时相隔 32 的情况更加是几乎不可能的事,平时做的分布式部署,一般也不会超过 10 台容器

注意:使用ip地址时要考虑到使用docker容器部署时ip可能会相同的情况。

  • 所有的节点共用一个数据库配置,每次节点重启,往mysql某个自建的表中新增一条数据,主键id自增并且自增id 要和 2^10-1 做按位与操作,防止总计重启次数超过 2^10 后溢出。使用这个自增的id作为机器码,这样能保证机器码绝对不重复。如果是TiDB这种分布式数据库(id自增分片不连续),按位与操作后,还要注意不能拿到相同的workId。

3. -1L ^ (-1L << x) 表示什么?

表示 x 位二进制可以表示多少个数值,假设x为3:

在计算机中,第一位是符号位,负数的反码是除了符号位,1变0,0变1, 而补码则是反码+1:

-1L 原码:1000 0001
-1L 反码:1111 1110
-1L 补码:1111 1111 ​​​​​​​​​​​​

从上面的结果可以知道,-1L其实在二进制里面其实就是全部为1,那么 -1L 左移动 3位,其实得到 1111 1000,也就是最后3位是0,再与-1L异或计算之后,其实得到的,就是后面3位全是1。-1L ^ (-1L << x)表示的其实就是x位全是1的值,也就是x位的二进制能表示的最大数值。

4.前端直接使用发生精度丢失

        如果前端直接使用服务端生成的long 类型 id,会发生精度丢失的问题,因为 JS 中Number是16位的(指的是十进制的数字),而雪花算法计算出来最长的数字是19位的,这个时候需要用 String 作为中间转换,输出到前端即可。

作者:筱白爱学习!!

欢迎关注转发评论点赞沟通,您的支持是筱白的动力!

​​​​​​​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/444446.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sql中 join 的简单用法总结(带例子)

join 常见的用法有&#xff1a; 目录 left join&#xff08;left outer join&#xff09;right join&#xff08;right outer join&#xff09;join&#xff08;inner join&#xff09;full join&#xff08;full outer join 、outer join&#xff09;cross join 说明&#xf…

docker自定义镜像

文章目录 一、自定义镜像1.1 镜像结构1.2 Dockerfile1.3 dockerCompose1.3.1 dockerCompose的作用1.3.2 dockerCompose的常用命令 1.4 镜像仓库 一、自定义镜像 1.1 镜像结构 自定义镜像通常包含三个基本部分&#xff1a;基础镜像、应用程序代码和配置文件。 基础镜像&#…

asp.net+sqlserver+C#网上洗衣店的设计与实现

选题的目的、理论与实践意义&#xff1a; 随着洗衣店服务的日渐完善和复杂&#xff0c;以前单纯的文本记录人工管理方式不仅效率低下&#xff0c;且易出错&#xff0c;直接导致管理费用的增加&#xff0c;服务质量的下降。由于这种人工管理方式不能完全适应需求的发展&#xff…

打包后dist包中app.**.js文件暴露大量接口信息,webpack-obfuscator对打包后的js代码混淆加密

问题描述 打包后dist包中app.**.js文件暴露大量接口信息&#xff0c;而webpack-obfuscator可以对打包后的js代码混淆加密 版本信息 webpack: 4.x.x node: 14.18.0 webpack4环境下使用webpack-obfuscator不能使用最新版本 我的下载版本是&#xff1a; npm install --save-de…

回溯算法模板(python)

#回溯模板&#xff0c;伪代码 def backtracking(参数):if (终止条件):存放结果return #如果要将数层中间的结果也插入&#xff0c;就不用写return&#xff0c;比如子集问题for (选择&#xff1a;本层集合中元素&#xff08;树中节点孩子的数量就是集合的大小&#xff09;):处…

通过Python的PIL库进行图像的过滤

文章目录 前言一、素材准备二、演示1.引入库2.定义图片路径3.打开原图4.过滤方法4.1图像的模糊效果代码效果图 4.2图像的轮廓效果代码效果图 4.3图像的细节效果代码效果图 4.4图像的边界效果代码效果图 4.5图像的边界加强效果代码效果图 4.6图像的阈值边界加强效果代码效果图 4…

自定义bean对象实现hadoop序列化

文章目录 一、源代码1.UserSaleMapper类2. UserSaleReducer类3. UserSaleDriver类4.pom.xml 二、执行结果 指导参考图&#xff1a; 一、源代码 1.UserSaleMapper类 package org.example.writable;import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Tex…

大厂对ChatGPT的开发利用和评估案例收录

ChatGPT已经进入各行各业&#xff0c;但是实际在工作中的有哪些应用呢&#xff1f;这里分享互联网一线大厂分享的一些实际使用案例&#xff0c;所有文章收录到 大厂对ChatGPT的开发利用和评估案例收录http://​www.webhub123.com/#/home/detail?projectHashid67792343&own…

Visual Studio 2019 C# 上位机入门(2):写一个简单的串口助手

前言 本文记录一下用Visual Studio 2019 C# 写一个简单的串口助手的过程&#xff0c;由于没有先从小处学习&#xff0c;而是直接找相关资料就开始做&#xff0c;免不了很多奇怪的问题花了一些时间&#xff0c;基于此情况&#xff0c;我将尽可能整理出更多细节&#xff0c;尤其…

Linux基础内容(20)—— 共享内存

Linux基础内容&#xff08;19&#xff09;—— 进程间通信(介绍与管道内容)_哈里沃克的博客-CSDN博客 目录 1.共享内存的原理 2.共享内存的概念和特点 创建共享内存 共享内存的形式 共享内存(ipc资源)的调用和特征 用户接口删除共享内存 共享内存关联 去关联 特点 …

React基础学习(一)

一、虚拟DOM和真实DOM <script type"text/babel"> // 此处一定要写babel!!!!!!!// 1. 创建虚拟DOM// const VDOM <h1 id"title">Hello, React!</h1> // 此处一定不要写引号 因为这不是字符串!!!!!!!const VDOM ( // 如果有多层嵌套&a…

PostgreSQL15.2最新版本安装_远程连接_Navicat操作_pgAdmin操作_Windows10上安装---PostgreSQL工作笔记001

首先去下载postgresql https://www.enterprisedb.com/downloads/postgres-postgresql-downloads 下载地址: 去上面的地址下载,最好下载10版本的,我这里下载的是15版本的,有问题,后面说吧 下载以后 然后双击安装 next 选择目录next next 输入密码next

好程序员:Java线下培训有必要吗?零基础想学Java怎么学?

有粉丝问好程序员&#xff1a;自己只有周末有时间&#xff0c;想报班学习Java编程&#xff0c;是线上学编程好还是线下学编程好&#xff1f;小源从实际客观以及学习效果的角度来讲&#xff0c;毫无疑问是线下学编程的效果会更好。为什么这样说呢&#xff1f; 比如&#xff1a;家…

初探强化学习

1.引言 人生中充满选择&#xff0c;每次选择就是一次决策&#xff0c;我们正是从一次次决策中&#xff0c;把自己带领到人生的下一段旅程中。在回忆往事的时候&#xff0c;我们会对生命中某些时刻的决策印象深刻&#xff1a;“还好当时选择了读研&#xff0c;毕业后找到了一份自…

学习小程序基础内容之逻辑交互

我们先来看一下实现的效果。 然后再来分享结构。 结构分为左右3:7 分配&#xff0c; 左侧是类别&#xff0c;右侧是该类别对应的品牌。 后台会在onload的请求把左侧的类别返回来&#xff0c;然后我们通过循环把数据展示出来。然后通过点击事件&#xff0c;把对应的品牌请求回来…

2023年,初级测试到高级测试开发工程师需要什么技能?卷起来......

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

泵站水闸自动化系统调试方案

1、调试方案概述 调试方案分为自控系统现场调试、视频系统现场调试以及控制中心远程调试&#xff08;包含通讯测试、自控调试和视频调试&#xff09;。 自控系统现场调试分为触摸屏调试和本地上位机调试两部分。触摸屏系统的调试步骤如下&#xff1a; 确认触摸屏和PLC的通讯情…

C++标准库 -- 泛型算法 (Primer C++ 第五版 · 阅读笔记)

C标准库 -- 泛型算法 (Primer C 第五版 阅读笔记&#xff09; 第10章 泛型算法------(持续更新)10.1、概述10.2、初识泛型算法10.2.1、只读算法10.2.2、写容器元素的算法10.2.3、重排容器元素的算法 10.3、定制操作10.4、再探迭代器10.5、泛型算法结构10.6、特定容器算法 第10…

【ubuntu】将硬盘挂载到指定目录并设置开机自动挂载

最近打算将数据盘开机自动挂载&#xff0c;省得每次都要手动挂载&#xff0c;总结步骤如下&#xff1a; 输入以下命令&#xff0c;将系统中所有的分区都列出来&#xff1a; sudo fdisk -l找到你要挂载的分区&#xff0c;可以通过容量大小分辨&#xff0c;当然&#xff0c;有可…

黑马Mysql从入门到高级

文章目录 1. 数据库基础1.1 基础概念1.2 SQL1.2.1 语法1.2.2 数据类型1.2.3 DDL&#xff08;definition&#xff09; 1.3 函数1.3.1 字符串函数1.3.2 日期函数1.3.3 数字函数1.3.4 流程函数 1.4 约束1.5 多表查询1.5.1 连接1.5.2 联合查询 2. 数据库进阶2.1 存储引擎2.1.1 Inno…