分布式ID生成策略-雪花算法Snowflake

news2024/9/24 6:12:10

分布式ID生成策略-雪花算法Snowflake

  • 一、其他分布式ID策略
    • 1.UUID
    • 2.数据库自增与优化
      • 2.1 优化1 - 共用id自增表
      • 2.2 优化2 - 分段获取id
    • 3.Reids的incr和incrby
  • 二、雪花算法Snowflake
    • 1.雪花算法的定义
    • 2.基础雪花算法源码解读
    • 3.并发1000测试
    • 4.如何设置机房和机器id
    • 4.雪花算法时钟回拨问题

这里主要总结雪花算法,其他的分布式ID策略不常用,这里简要描述,而且各大公司生产基本都是选择雪花算法,所以这里针对雪花算法进行详细解读,其他常见的分布式id策略则只做简略描述。

一、其他分布式ID策略

分布式ID是分布式架构中比较基础和重要的场景,好的分布式ID策略可以提供更强大的并发,保障业务的正常展开。各大公司最为常用的是雪花算法,和在雪花算法基础上进行改进的算法,当然也有其他的比如数据库自增等,这里先对其他分布式ID策略的简述,这样才能更清晰比对和雪花算法的差异。

1.UUID

UUID是一串32个字符,128位的随机字符串。UUID在数据库比较小并发量不高的服务中使用是完全可以的,他的最大的特点就是简单易用,使用简洁,对于数据量不大的系统推荐使用,比如OA等公司内部系统。JDK自带UUID的api,可以直接使用:

package com.cheng.common.snowflake.api.test;

import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.UUID;

/**
 * @author pcc
 * @version 1.0.0
 * @description 描述下这个类吧
 */
@SpringBootTest
public class TestUUID {

    @Test
    public void testUUID() {
        System.out.println(UUID.randomUUID());
    }
}

只需要上面一行简单的代码就可以获取到UUID了,使用起来可以说是非常简单了。
在这里插入图片描述
优点:简单易用、生成效率高
缺点:字符串随机生成,当数据量特别大时使用mysql数据库,插入效率会比较低下,原因是因为Mysql的索引是B+Tree,B+Tree所有数据都存储在叶子节点上,且按顺序排列,若是随机字符串会增加寻址成本,造成插入效率低下,因为是随机字符串在进行范围查询时索引效率也很低下。
使用总结:不推荐使用,如果用只能用在数据量比较小的服务,这是一个悖论,数据量小也没有必要使用分布式服务分布式id了,直接使用数据库自增就行,所以这里不推荐使用。

2.数据库自增与优化

这里的数据库自增就是指数据库的auto_increment,当我们为主键设置auto_increment时,那么这个主键就会随着表记录创建而填充且id是逐个递增的,数据量不大的情况是是完全可以使用这种方式的,但当有分库分表时,数据量和并发量大时数据写入就会变慢,因为首先单库的并发量是有限的,其次单表数据量增大后单表操作就会越来越慢,也会影响性能,所以需要对数据库进行垂直和水平拆分(分库分表)。

2.1 优化1 - 共用id自增表

上面的数据库自增很显然在分库分表时是无法满足数据库id唯一且自增的,因为是多个库多个表,所以这里需要进行优化,优化的方式也比较简单,就是单独使用一个表来维护id,数据插入之前通过这个表来分配数据库的id。这样就可以保证多个库多个表的主键的不冲突了,但当并发量继续增高时,即使这个表单独只做id分配也会吃力,此时还需要为这个表提供优化,此时还可以考虑将这个表所在的库做主-主的设置,来提升并发能力。
那这个优化的核心其实就是获取id的这个表了。这种方式会将表结构设置为如下:

CREATE TABLE `gene_id` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `stub` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `stub` (`stub`)
);

这里需要注意两点:

  • 主键需要自增AUTO_INCREMENT,因为id需要靠这个表来维持,所以需要设置id自增
  • stub需要唯一约束,这个字段是需要根据值进行替换达到id自增的目的的辅助列,所以需要唯一约束(根据唯一值进行替换)。

表结构出来以后,需要处理的是如何正确的自增id,网上的说法都是使用这个:

BEGIN;
REPLACE INTO gene_id (stub) values ('stub');
SELECT LAST_INSERT_ID();
COMMIT;

这里简单说下这个sql,REPLACE INTO的作用是如果存在则删除重新插入,如果不存在则直接插入。所以可以实现id主键的自增。LAST_INSERT_ID则是msyql内置的方法可以获取到上一次增加的id,使用这种方式可以一次获取到我们想要的id
思考:这个获取id的方式会不会存在幻读问题
假如我们不使用上面这个方式获取id,我们使用的是select 通过id倒排获取最大id是很可能会出现幻读的问题的,幻读就会导致大家可能获取的id是同一个,从而出现了数据id的重复。那这里会不会呢,这里其实是不会的,因为使用的是LAST_INSERT_ID,这个方法只会返回当前事物中新增的id,对于其他事务的新增id并不会返回,所以不会有幻读的问题,所以使用这种方式获取自增的id才是正确的姿势。

2.2 优化2 - 分段获取id

上面的2.1 其实还可以继续优化,而且这里的优化也是美团实际在用的,废话不多说,如何优化呢?核心思想就是减少和数据库的交互,从而提升性能,现在是获取一次id就需要和id管理表交互一次,优化的思想是分段获取id,比如一次获取1000个id,那么就可以减少获取id数量的999次交互,而获取到的1000个id可以放入到内存中,当需要时从内存中获取id,这样无疑会提升很多性能。
这种方式也可能会面临临界点时id获取多线程阻塞问题,可以通过提前加载的方式来解决这种问题。

3.Reids的incr和incrby

Redis因为真正执行CRUD的操作是单线程,所以他的操作是原子性的,此时我们可以利用他的命令incr(redis的++操作),incrby(incrby key 10意思是将key的值增加10),这种命令来实现和数据库类似的id的自增,而且redis也是支持持久化的,我们可以同时开启redis的rdb和aof来保证数据的不丢失。而且redis本身也是支持高并发的,对redis进行集群扩展也比较方便,所以使用redis也是可以的。

二、雪花算法Snowflake

1.雪花算法的定义

雪花算法最早由Twitter开源,用于产生一个64bit的整数(换算成10进制则是19位)。同时64bit在java中正好是long的长度(long是8字节,一字节是8bit),在数据库mysql中正好是BIGINT的长度。在雪花算法中64bit的整数被划分了4部分:1位符号位+41位时间戳位+10位机器位+12位随机数位,如下:
在这里插入图片描述

  • 1位符号位:
    二进制数据中首位表示正负,这里是0不可变

  • 41位的时间戳:
    41位用来标识时间戳,最大值是2的41次方-1为:2199023255552,转换成时间戳的话是2039-09-07 23:47:35,所以这个41为的时间戳如果不特殊处理可以表示最大的范围就是到2039年(从1970年算是69年),
    在这里插入图片描述

    2039年对于大部分公司来说肯定是不行的(不能说用到2039年就不用了吧)所以一般这个41位的时间戳不会直接用当前的时间戳来直接填充,而是使用当前时间戳减去一个默认的时间戳这样就可以获得更大的表示范围了,这个默认的时间戳通常是系统的上线时间,假如系统上线时间是2024年3月1号,根据上面的69年的表示范围那么这个分布式id的时间范围就可以表示到2093年。2093年对于任何公司来说都是可用的了,尚不说公司能不能存在到那时候,即使存在了系统肯定也早需要重构了,不会有任何系统给你用这么久的。

  • 10位的机器位:
    机器位最大为10位,一般做法是5位用于机房id的标识,5位用于机器id的标识。这样无论是机房和机器都可以最大容纳2的五次方减1(31)的数量。不过实际使用时可以根据实际情况进行调整,因为机房数量一般也到不了31,就是机器数量到达31的也不多。所以可以根据实际情况来进行调整机器位10个bit的分配。

  • 12位的随机数:
    12位的随机数最大可以表示2的12次方减1的数据(4095)所以也就是说最大我们可以在1ms内产生4095个id(时间戳位是ms),那么1s内就是4095000≈400W。而且这是单台机器上的每秒可产生的不重复id,如果横向扩展机器的话,这个值还会更大。所以12位的随机数位是肯定够用的了,当然真正使用时是不能使用随机数的,而是应该进行整数的自增,这样才能保证不重复。

总结一句话就是雪花算法是一个可以在单机每秒钟最高产生400w不重复id的id生成算法(假如机器性能扛得住)。在横向扩展后这个值会更大,如果是3台机器则是1200w,所以分布式id基本上可以适用任何并发场景。

2.基础雪花算法源码解读

雪花算法并不难,只需要知道生成策略其实大部分人应该都是可以写出来的,所以说最重要的一直不是动手的能力而是你思维的能力,可以做到的远比可以想到的要多得多。

package com.cheng.ebbing.message.snowflake;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author pcc
 * @version 1.0.0
 * @description 雪花算法生成id
 */
public class SnowflakeIdGenerator {
    // 起始的时间戳
    private final long twepoch = 1288834974657L;

    // 每一部分占用的位数
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;

    // 每一部分的最大值
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private final long maxSequence = -1L ^ (-1L << sequenceBits);

    // 每一部分向左的位移
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 记录上一次生成ID的时间戳
    private long lastTimestamp = -1L;

    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    private final long datacenterId;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("Worker ID can't be greater than " + maxWorkerId + " or less than 0");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID can't be greater than " + maxDatacenterId + " or less than 0");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long generateId() {
        long timestamp = timeGen();

        // 时钟回拨直接异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds.");
        }

        if (timestamp == lastTimestamp) {
	        // 按位与只要都为1才为1否则为0
	        // 4095的十进制数在二进制中表示为111111111111,而4096的十进制数在二进制中表示为1000000000000。
            sequence = (sequence + 1) & maxSequence;

            if (sequence == 0L) {
                // 当前毫秒的序列号已经用完,等待下一毫秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒内,序列号重置为0
            sequence = 0L;
        }

        // 记录上一次生成ID的时间戳
        lastTimestamp = timestamp;

        // 计算时间戳左移22位,加上数据中心ID左移17位,加上机器ID左移12位,加上序列号
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

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

上面是一个雪花算法的生成源码很简单,应该是一看就懂了,唯一可能需要说的是生成id的时候这个对于位运算和按位或操作不熟悉的可能有些懵,之类简单说下。二进制的位运算可以类比十进制的乘以10的操作,假如11(十进制的3)这个二进制数左移两位则表示在末尾添加两个00,也就是1100(十进制12),不清楚的这么记就行。而按位或则是对比操作的两个数的相同位,有一个为1则记为1,否则为0,这里左移以后末尾补零,左移按位或就可以理解为10进制的加法了,相同与有一个10禁止的1乘以100以后,在他的个位和10位上进行加数。

3.并发1000测试

这里使用1000个线程并发来压测,其实肯定不会有重复的,这里将产生的id放入到ConcurrentHashMap的key中,如果最后key的数量和线程数保持一致,则说明这个源码没有问题:

// 示例用法
    public static void main(String[] args) {
        // 数据中心ID和机器ID分别为1和1
        SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1); 
        // 用于存放产生的id
        ConcurrentHashMap<Long, Long>  ids = new ConcurrentHashMap<>();
        // 假设有1000个线程同时生成id,那么这时候测试下是否有重复的id
        for (int i = 0; i < 1000; i++) {
            new Thread(
                    ()->{
                        ids.put(idGenerator.generateId(),1L);
                    }
            ).start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 如果重复,那么肯定是小于线程数的
        System.out.println("生成的id数量:" + ids.size());
    }

测试结果自然是和我们声明的线程量是一致的。

生成的id数量:1000

进程已结束,退出代码0

上面的id生成例子用起来其实没啥大问题,但一般也不会直接用,还需要考虑如下一些场景的适配。

4.如何设置机房和机器id

这里得机器id和机房id都是通过传入的,那生产环境该如何定义这个值呢,主要是两种方案:

  • 1.通过注册中心定义
    分布式id的服务如果有多个,可以都注册到注册中心里,在服务启动后获取所有实例,根据实例ip进行排序然后分配机器id和机房id。
  • 2.服务器直接定义
    也可以直接在机器上定义一个变量,项目中根据服务器上定义的变量定义机器id和服务器id,springboot配置文件中使用${workid}这种方式来获取机器上定义的环境变量。

4.雪花算法时钟回拨问题

时钟回拨问题是指,单台机子上时间出现回退导致id出现重复的问题,可以说只要时间回退了id重复的概率基本是99%了,此外不要想着时间回退不容易碰到,这个基本都会碰到。有以下几种可能会出现时间回退:

  • 1.润秒:
    世界时间(国际原子时)与地球自转时间(世界时)之间存在微小差异。为了使时间保持同步,国际原子时不时地进行调整,即通过插入或删除“润秒”(leap second)来实现,这种如果是删除就会导致时间回退。
  • 2.时间漂移
    服务器因硬件、温度问题导致的时间不一致
  • 3.时间同步问题
    每个电脑都不是和标准时间实时同步的,都是间隔一段时间去同步一次,这个时间也是可能出现回退的。
  • 4.手动调整
    这种也有可能发生,服务器管理员进行了手动调整,且时间向后调整了

所以说时间回退是很可能会碰到的,这个问题也是必须要解决的,而上面的代码是没有解决这个问题的。时间回退造成的问题是出现了相同的时间戳,而因为是同一台机器同一个机房所以id重复概率很高。下面来说下通常解决时间回退问题的解决方案。

  • 方案一:阻塞等待
    这个适合时间回退没有太久时,可以在上面代码中进行判断下,根据自己的业务并发量进行计算下看看可以接受多大时间的阻塞而不会影响线上的运行。
  • 方案二:id接续生成
    这个需要记录下每个ms内id生成的最大数,当然这个数量也不能存储过多,顶多存储个几十秒的每毫秒的最大id。当出现时间回退时,我们可以接着出现回退的ms内的最大id继续生成,这样也不会有id重复的问题,但是如果时间回退的较多使用这种方式也是不合适的。
  • 方案三:预留时间回退位
    当时间回退较多时,无论是阻塞还是id接续生成都是不合适的,此时还可以考虑针对64位的数据进行预留时间回退位置,比如我们可以在10位的机器和机房id中预留2位用以标识时间回退,让机房id和机器id只占8位具体如何分配可以根据自己实际情况来说,当出现时间回退时可以打开该标识。
  • 方案四:下线时间回退机器
    这个是有风险的,虽然下线了服务器,但是如果是因为润秒原因导致的时间回退,很可能会导致大片机器同时下线,所以这种方式是很有安全隐患的。

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

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

相关文章

在Ubuntu22.04安装Fcitx5中文输入法教程(十分详细)

前言 书接上回&#xff0c;一时兴起将主力机的 Ubuntu 20.04 LTS 升级至了刚刚发布的 22.04 LTS。从 X 切换到 Wayland 、GNOME 从 3.36 升级至 42、Python 默认为 3.10 等等……使用太新的软件包反而暂时带来了麻烦&#xff0c;部分原有的软件和插件都不可用了。这其中就包括…

浅谈马尔科夫链蒙特卡罗方法(MCMC)算法的理解

1.解决的问题 计算机怎么在任意给定的概率分布P上采样&#xff1f;首先可以想到把它拆成两步&#xff1a; &#xff08;1&#xff09;首先等概率的从采样区间里取一个待定样本x&#xff0c;并得到它的概率为p(x) &#xff08;2&#xff09;然后在均匀分布U[0,1]上取一个值&a…

可视化管理的kanban插件 | Obsidian实践

继上一篇文章之后&#xff0c;有朋友提问说&#xff1a; 刚好&#xff0c;关于kanban插件的素材&#xff0c;是之前一早就准备好的&#xff0c;便快速整理成文&#xff0c;简单分享一下个人实践。 Prompt&#xff1a;项目流程管理看板&#xff0c;色彩鲜艳。by 通义万象 看板管…

C++调用lua函数

C 调用Lua全局变量(普通) lua_getglobal(lua, "width");int width lua_tointeger(lua,-1);lua_pop(lua,1);std::cout << width << std::endl;lua_close(lua); 这几行代码要放到lua_pcall(lua, 0,0,0);之后才可以. C给lua传递变量 lua_pushstring(lua, …

三、低代码平台-单据配置(单表增删改查)

一、业务效果图 主界面 二、配置过程简介 配置流程&#xff1a;业务表设计 -》业务对象建立-》业务单据配置-》菜单配置。 a、业务表设计 b、业务对象建立 c、业务单据配置 功能路径&#xff1a;低代码开发平台/业务开发配置/单据配置维护 d、菜单配置

【JavaEE】_Spring MVC 项目传参问题

目录 1. 传递单个参数 1.1 关于参数名的问题 2. 传递多个参数 2.1 关于参数顺序的问题 2.2 关于基本类型与包装类的问题 3. 使用对象传参 4. 后端参数重命名问题 4.1 关于RequestPara注解 1. 传递单个参数 现创建Spring MVC项目&#xff0c;.java文件内容如下&#xff…

【k8s存储--使用OpenEBS做持久化存储】

1、简介 使用OpenEBS&#xff0c;你可以将有持久化数据的容器&#xff0c;像对待其他普通容器一样来对待。OpenEBS本身也是通过容器来部署的&#xff0c;支持Kubernetes、Swarm、Mesos、Rancher编排调度&#xff0c;存储服务可以分派给每个pod、应用程序、集群或者容器级别&am…

Mybatis plus拓展功能-JSON处理器

目录 1 前言 2 使用方法 2.1 定义json实体类 2.2 在实体类中使用 1 前言 这是我最近学到的比较新奇的一个东西&#xff0c;数据库居然还可以存储JSON格式的数据&#xff0c;如下。虽然我感觉一般也没谁会这样干&#xff0c;但是既然有&#xff0c;那就当个科普讲一下Mybat…

【自然语言处理】NLP入门(三):1、正则表达式与Python中的实现(3):字符转义符及进制转换

文章目录 一、前言二、正则表达式与Python中的实现1.字符串构造2. 字符串截取3. 字符串格式化输出4. 字符转义符a. 常用字符转义符续行符换行符制表符双引号单引号反斜杠符号回车符退格符 b. ASCII编码转义字符进制转换2 进制8 进制10 进制16 进制进制转换函数 c. Unicode字符\…

每天学习一个Linux命令之gunzip

每天学习一个Linux命令之gunzip 在Linux系统中&#xff0c;有许多强大且常用的命令&#xff0c;其中之一是gunzip。gunzip命令用于解压缩.gz文件&#xff0c;它是gzip的伴生命令之一。本篇博客将详细介绍gunzip命令及其可用的选项&#xff0c;以帮助您更好地理解和使用这个命令…

运放设计选型中关注的参数-运算放大器选型参数

1、直流增益&#xff08;AVD&#xff09; 直流增益是运放最重要一个属性之一&#xff0c;其定义为输出电压的变化与输入电压变化之比值&#xff0c;通常用V/mV表示这个比值&#xff0c;例如&#xff0c;增益为30000&#xff0c;可表示为30V/mV&#xff0c;有些地方也会把增益用…

实践航拍小目标检测,基于轻量级YOLOv7tiny开发构建无人机航拍场景下的小目标检测识别分析系统

关于无人机相关的场景在我们之前的博文也有一些比较早期的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《deepLabV3Plus实现无人机航拍目标分割识别系统》 《基于目标检测的无人机航拍场景下小目标检测实践》 《助力环保河道水质监测&#xff0c;基于yolov…

搜索旋转排序数组[中等]

优质博文IT-BLOG-CN 一、题目 整数数组nums按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums在预先未知的某个下标k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转&#xff0c;使数组变为[nums[k], nums[k1], ..., nums[n-…

AI蠕虫病毒威胁升级,揭示AI安全新危机

一组研究人员成功研发出首个能够通过电子邮件客户端窃取数据、传播恶意软件以及向他人发送垃圾邮件的AI蠕虫&#xff0c;并在使用流行的大规模语言模型&#xff08;LLMs&#xff09;的测试环境中展示了其按设计功能运作的能力。基于他们的研究成果&#xff0c;研究人员向生成式…

HTML+CSS+JS:日夜交替

效果演示 实现了一个简单的日夜交替效果的动画。页面中包含了太阳、月亮和海洋的元素&#xff0c;通过切换按钮可以切换页面的主题&#xff0c;从白天切换到黑夜&#xff0c;或者从黑夜切换到白天。 Code <div class"btn-box"><div class"sunBtn"…

Docker将本地的镜像上传到私有仓库

使用register镜像创建私有仓库 [rootopenEuler-node1 ~]# docker run --restartalways -d -p 5000:5000 -v /opt/data/regostry:/var/lib/registry registry:2[rootopenEuler-node1 ~]# docker images REPOSITORY TAG IMAGE…

【数据结构与算法】常见排序算法(Sorting Algorithm)

文章目录 相关概念1. 冒泡排序&#xff08;Bubble Sort&#xff09;2. 直接插入排序&#xff08;Insertion Sort&#xff09;3. 希尔排序&#xff08;Shell Sort&#xff09;4. 直接选择排序&#xff08;Selection Sort&#xff09;5. 堆排序&#xff08;Heap Sort&#xff09;…

【代码】Android|判断asserts下的文件存在与否,以及普通文件存在与否

作者版本&#xff1a;Android 11及以上 主要是发现网上没有完整的、能跑的代码&#xff0c;不知道怎么回事&#xff0c;GPT给我重写的。我只能保证这个代码尊嘟能跑&#xff0c;不像其他的缺胳膊少腿的。 asserts 贴一下结果&#xff1a; boolean isAssertFileExists(String …

CNN-LSTM-Attention混合神经网络归时序预测的MATLAB实现(源代码)

CNN-LSTM-Attention介绍&#xff1a; CNN-LSTM-Attention混合神经网络是一种结合了卷积神经网络&#xff08;CNN&#xff09;、长短期记忆神经网络&#xff08;LSTM&#xff09;和注意力机制&#xff08;Attention&#xff09;的模型。这种混合神经网络结合了CNN对空间特征的提…