分布式 ID 生成系统 Leaf 的设计思路,源码解读

news2025/1/14 0:46:08

什么是分布式ID?

ID 最大的特点是 唯一

而分布式 ID,就是指分布式系统下的 ID,它是 全局唯一 的。

为啥需要分布式ID呢?

这就和 唯一 息息相关了。

比如我们用 MySQL 存储数据,一开始数据量不大,但是业务经过一段时间的发展,单表数据每日剧增,最终突破 1000w,2000w …… 系统开始变慢了,此时我们已经尝试了 优化索引读写分离升级硬件升级网络 等操作,但是 单表瓶颈 还是来了,我们只能去 分库分表 了。

而问题也随着而来了,分库分表后,如果还用 数据库自增ID 的方式的话,那么在用户表中,就会出现 两个不同的用户有相同的ID 的情况,这个是不能接受的。

分布式ID全局唯一 的特点,正是我们所需要的。

分布式ID的生成方式

  • UUID
  • 数据库自增ID (MySQL,Redis)
  • 雪花算法

基本就上面几种了,UUID 的最大缺点就是太长,36个字符长度,而且无序,不适合。

而其他两种的缺点还有办法补救,可能这也是 Leaf 提供这两种生成 ID 方式的原因。

项目简介

Leaf ,分布式 ID 生成系统,有两种生成 ID 的方式:

  1. 号段模式
  2. Snowflake模式

号段模式

数据库自增ID 的基础上进行优化

  1. 增加一个 segement ,减少访问数据库的次数。
  2. 双 Buffer 优化,提前缓存下一个 Segement,降低网络请求的耗时(降低系统的TP999指标)

biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度

没优化前,每次都从 db 获取,现在获取的频率和 step 字段相关。

双 Buffer 优化思路 👇

号段模式源码解读

SegmentService 构造方法

作用 👇

  1. 配置 dataSource
  2. 设置 MyBatis
  3. 实例化 SegmentIDGenImpl
  4. 执行 init 方法

这段代码我也忘了 哈哈,已经多久没直接用 mybatis 了,还是重新去官网翻看的。

实例化 SegmentIDGenImpl 时,其中有两个变量要留意下 👇

  1. SEGMENT_DURATION,智能调节 step 的关键
  2. cache ,其中 SegmentBuffer 是双 Buffer 的关键设计。

这里先不展开,看看 init 方法先。

SegmentIDGenImpl init 方法

作用 👇

  1. 执行 updateCacheFromDb 方法
  2. 开后台线程,每分钟执行一次 updateCacheFromDb() 方法

显然,核心在 updateCacheFromDb

updateCacheFromDb 方法

这里就直接看源码和我加的注释

private void updateCacheFromDb() {
        logger.info("update cache from db");
        StopWatch sw = new Slf4JStopWatch();
        try {
            // 执行 SELECT biz_tag FROM leaf_alloc 语句,获取所有的 业务字段。
            List<String> dbTags = dao.getAllTags();
            if (dbTags == null || dbTags.isEmpty()) {
                return;
            }
            // 缓存中的 biz_tag
            List<String> cacheTags = new ArrayList<String>(cache.keySet());
            // 要插入的 db 中的 biz_tag
            Set<String> insertTagsSet = new HashSet<>(dbTags);
            // 要移除的缓存中的 biz_tag 
            Set<String> removeTagsSet = new HashSet<>(cacheTags);
​
            // 缓存中有的话,不用再插入,从 insertTagsSet 中移除
            for (int i = 0; i < cacheTags.size(); i++) {
                String tmp = cacheTags.get(i);
                if (insertTagsSet.contains(tmp)) {
                    insertTagsSet.remove(tmp);
                }
            }
            
            // 为新增的 biz_tag 创建缓存 SegmentBuffer
            for (String tag : insertTagsSet) {
                SegmentBuffer buffer = new SegmentBuffer();
                buffer.setKey(tag);
                Segment segment = buffer.getCurrent();
                segment.setValue(new AtomicLong(0));
                segment.setMax(0);
                segment.setStep(0);
                cache.put(tag, buffer);
                logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
            }
​
            
            // db中存在的,从要移除的 removeTagsSet 移除。
            for (int i = 0; i < dbTags.size(); i++) {
                String tmp = dbTags.get(i);
                if (removeTagsSet.contains(tmp)) {
                    removeTagsSet.remove(tmp);
                }
            }
            
            // 从 cache 中移除不存在的 bit_tag。
            for (String tag : removeTagsSet) {
                cache.remove(tag);
                logger.info("Remove tag {} from IdCache", tag);
            }
        } catch (Exception e) {
            logger.warn("update cache from db exception", e);
        } finally {
            sw.stop("updateCacheFromDb");
        }
    }
复制代码

执行完后,会出现这样的 log

Add tag leaf-segment-test from db to IdCache, SegmentBuffer SegmentBuffer{key='leaf-segment-test', segments=[Segment(value:0,max:0,step:0), Segment(value:0,max:0,step:0)], currentPos=0, nextReady=false, initOk=false, threadRunning=false, step=0, minStep=0, updateTimestamp=0}
复制代码

最后 init 方法结束后,会将 initOk 设置为 true


项目启动完毕后,我们就可以调用这个 API 了。

如图,访问 LeafController 中的 Segment API,可以获取到一个 id。

SegmentIDGenImpl get 方法

可以看到,init 不成功会报错。

以及会直接从 cache 中查找这个 key(biz_tag) , 没有的话会报错。

拿到这个 SegmentBuffer 时,还得看看它 init 了 没有,没有的话用双检查锁的方式去更新

先来看下一眼 SegmentBuffer 的结构 👇

SegmentBuffer 类

⭐updateSegmentFromDb 方法

这里就是更新缓存的方法了,主要是更新 Segment 的 value , max,step 字段。

可以看到有三个 if 分支,下面展开说

分支一:初始化

第一次,buffer 还没 init,如上图,执行完后会更新 SegmentBuffer 的 step 和 minStep 字段。

分支二:第二次更新

这里主要是更新这个 updateTimestamp ,它的作用看分支三

分支三:剩下的更新

这里就比较有意思了,就是说如果这个号段在 15分钟 内用完了,那么它会扩大这个 step (不超过 10w),创建一个更大的 MaxId ,降低访问 DB 的频率。

那么,到这里,我们完成了 updateSegmentFromDb 方法,更新了 Segment 的 value , max,step 字段。

但是,我们不是每次 get 都走上面的流程,它还得走这个缓存方法 👇

⭐getIdFromSegmentBuffer 方法

显然,这是另一个重点。

如图,在死循环中,先获取读锁,拿到当前的号段 Segment,进行判断

  • 使用超过 10% 就开新线程去更新下一个号段
  • 没超过则将 value (AtomicLong 类型)+1 ,小于 maxId 则直接返回。

这里要重点留意 读写锁的使用 ,比如 开新线程时,使用了这个 写锁 ,里面的 nextReady 等变量使用了 volatile 修饰

这里的核心就是切换 Segment。

至此,号段模式结束。

优缺点

信息安全如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。—— 《Leaf——美团点评分布式ID生成系统》

可以看到,这个号段模式的最大弊端就是 信息不安全,所以在使用时得三思,能不能用到这些业务中去。


Snowflake模式

雪花算法,核心就是将 64bit 分段,用来表示时间,机器,序列号等。

41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表示1024台机器。

12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为 2^12 * 1000 = 409.6w/s

这里使用 Zookeeper 持久顺序节点的特性自动对 snowflake 节点配置 wokerID,不用手动配置。

时钟回拨问题

Snowflake模式源码解读

这部分源码就不一一展开了,直接展示核心代码

SnowflakeZookeeperHolder init 方法

这里要注意调整这个 connectionTimeoutMs 和 sessionTimeoutMs ,不然两种模式都启动的话,这个 zk 的 session 可能会超时,造成启动失败。

图中流程 👇

  1. 看看 zk 节点存不存在,不存在就创建
  2. 同时将 worker id 保存到本地。
  3. 创建定时任务,更新 znode。

SnowflakeIDGenImpl get 方法

这里直接看代码和注释了 👇

@Override
    public synchronized Result get(String key) {
        long timestamp = timeGen();
        //  发生了回拨,此刻时间小于上次发号时间
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    //时间偏差大小小于5ms,则等待两倍时间
                    wait(offset << 1);
                    timestamp = timeGen();
                    //还是小于,抛异常并上报
                    if (timestamp < lastTimestamp) {
                        return new Result(-1, Status.EXCEPTION);
                    }
                } catch (InterruptedException e) {
                    LOGGER.error("wait interrupted");
                    return new Result(-2, Status.EXCEPTION);
                }
            } else {
                return new Result(-3, Status.EXCEPTION);
            }
        }
        if (lastTimestamp == timestamp) {
            // sequenceMask = ~(-1L << 12 ) = 4095 二进制即 12 个1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                //seq 为0的时候表示是下一毫秒时间开始对seq做随机
                sequence = RANDOM.nextInt(100);
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            //如果是新的ms开始
            sequence = RANDOM.nextInt(100);
        }
        lastTimestamp = timestamp;
        // timestampLeftShift = 22, workerIdShift = 12 
        long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
        return new Result(id, Status.SUCCESS);
    }
​
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
​
    protected long timeGen() {
        return System.currentTimeMillis();
    }
复制代码

API 效果

生成 ID

反解 ID

至此,这个 Snowflake 模式也了解完毕了。

总结

看完上面两种模式,我觉得两种模式都有它适用的场景,号段模式更适合对内使用(比如 用户ID),而如果你这个 ID 会被用户看到,暴露出去有其他风险(比如爬虫恶意爬取等),那就得多斟酌了,。而订单号 就更适合用 snowflake 模式。

分布式ID 的特点

  1. 全局唯一
  2. 趋势递增(有序一直很重要,粗略有序还是严格有序就看情况了)
  3. 可反解(可选)
  4. 信息安全(可选)

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

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

相关文章

别再纠结线程池大小 + 线程数量了,没有固定公式的

可能很多人都看到过一个线程数设置的理论&#xff1a; CPU 密集型的程序 - 核心数 1I/O 密集型的程序 - 核心数 * 2 不会吧&#xff0c;不会吧&#xff0c;真的有人按照这个理论规划线程数&#xff1f; 线程数和CPU利用率的小测试 抛开一些操作系统&#xff0c;计算机原理不…

水资源税取水计量监管系统 取用水户水量在线监测平台 水资源远程实时监控管理系统

平升电子水资源税取水计量监管系统/取用水户水量在线监测平台/水资源远程实时监控管理系统适用于水资源管理部门对地下水和地表水的用水量、水位、水质进行监测&#xff0c;还可扩展远程或自动控制泵/闸/阀实现用水量控制。系统帮助管理部门掌握所辖区域内水资源取用水情况&…

打电话用蓝牙耳机什么牌子好?打电话清晰的蓝牙耳机推荐

随着蓝牙耳机的普及&#xff0c;我们可以享受到沉浸式的音乐。在不打扰任何人的情况下&#xff0c;尽情的享受&#xff0c;使用蓝牙耳机有时候避免不了来电&#xff0c;为了保证通话的清晰&#xff0c;许多人在选购的时候也会更加的看重麦克风&#xff0c;下面小编整理了几款打…

如何使用JMeter操作Elasticsearch

JMeter是Apache组织基于Java开发的压力测试工具&#xff0c;用于对软件做压力测试&#xff0c;Elasticsearch是一个分布式、高扩展、高实时的搜索与数据分析引擎(简称ES)&#xff0c;下面来展示最基本的用JMeter操作ES示例。 打开JMeter工具&#xff0c;在测试计划下添加“线程…

【金万维】使用天联高级版登录U8,进行凭证打印操作。

【操作步骤】 通过“天联高级版客户端”登录 U8&#xff0c;打印凭证步骤&#xff1a; 第一步&#xff1a;首先查看一下天联高级版客户端的打印参数是否如下图所示。 &#xff08;一般软件初次安装后&#xff0c;默认即可。&#xff09; 第二步&#xff1a;进入U8后&#xff0…

web概述20

MVC模式 MVC全名是Model View Controller是模型视图控制器的缩写&#xff0c;是一种软件设计典范&#xff0c;是一种架构型的模式&#xff0c;本身不引入新功能&#xff0c;只是帮助将开发的结构组织的更加合理。 它使用一种业务逻辑、数据、界面显示分离的方法&#xff0c;将…

麦芽糖-聚乙二醇-顺铂 cisplatin-PEG-maltose

麦芽糖-聚乙二醇-顺铂 cisplatin-PEG-maltose 中文名称&#xff1a;麦芽糖-顺铂 英文名称&#xff1a;maltose-cisplatin 别称&#xff1a;生物素修饰麦芽糖 生物素-麦芽糖 麦芽糖-聚乙二醇-顺铂 cisplatin-PEG-maltose 顺铂-PEG-麦芽糖 纯度&#xff1a;95% 存储条件…

电动车充电费到了涨价的时候了,低能源使用成本正在成为过去

电动汽车以省钱成为各个新能源汽车企业吹嘘的宣传点&#xff0c;然而电动汽车车主如今正面临公共充电桩短缺的问题&#xff0c;公共充电桩的建设跟不上电动汽车增长的速度&#xff0c;导致电动汽车车主充电难问题日益突出&#xff0c;解决这个问题就只能通过涨价来解决供应短缺…

ln命令应用

记录&#xff1a;352 场景&#xff1a;在CentOS 7.9操作系统上&#xff0c;使用ln命令创建软链接(symbolic links)和硬链接(hard links)。解决&#xff1a;Too many levels of symbolic links。 版本&#xff1a; 操作系统&#xff1a;CentOS 7.9 1.命令应用 (1)目录创建软…

mybatis-plus,sgg,杨bochao,p5完成

一 MyBatis-Plus简介 增强工具。只做增强不做改。 可以直接在mybatis的基础上整合mybatis-plus。此时并不会影响mybatis的功能&#xff0c;即mybatis原来的功能都在&#xff0c;该怎么用还怎么用。锦上添花的是还能使用mybatis-plus提供的&#xff1a;通用的mapper、通用的ser…

drone+github实现自动化部署

目录drone简介drone与jenkins对比创建oauth2服务器安装drone生成drone的共享密钥配置drone.yml文件安装drone项目配置配置Dockefile配置.drone.yml文件配置阿里云镜像拉取文件drone简介 drone与jenkins对比 创建oauth2 在github中设置第三方app配置。 需要记住id…

[附源码]Python计算机毕业设计Django车源后台管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

微信截图无法发送,也发不出电脑上的图片

微信截图无法发送&#xff0c;也发不出电脑上的图片 现象 今天微信突然出现这个问题&#xff0c;怎么改设置都调不好&#xff0c;卸载重装都不行&#xff0c;最后发现&#xff0c;微信的消息目录中&#xff0c;一些文件无法删除&#xff0c;提示“文件或目录损坏且无法读取”…

上市公司排污费2010-2020重污染行业环境披露水平-原始数据及计算结果

一、重污染行业环境信息披露水平 1、数据来源&#xff1a;见附件 2、时间跨度&#xff1a;2009-2020 3、区域范围&#xff1a;重污染行业的认定主要依据环境保护部2008年6月发布的《上市公司环保核查行业分类管理名录》&#xff08;环办函[2008]373号&#xff09;、《上市公…

PDF转TXT怎么转?看完这篇你就会了

我们在学习工作的时候&#xff0c;经常会使用PDF格式来传输文件&#xff0c;因为PDF具有较强的格式稳定性。可是有时候我们也需要将PDF转成TXT格式&#xff0c;因为有些电子书阅读器比较适合阅读TXT格式的文件。那你们知道PDF怎么转TXT吗&#xff1f;有需要转换PDF格式的小伙伴…

Apollo 应用与源码分析:Monitor监控-软件监控-进程存活监控-process_monitor

目录 流程 代码 分析 获取可以运行的进程的信息 检查HMI 的模块信息 检查被监控的组件 检查其他组件 判断进程状态UpdateStatus 流程 代码 class ProcessMonitor : public RecurrentRunner {public:ProcessMonitor();void RunOnce(const double current_time) overrid…

BUUCTF Misc 被劫持的神秘礼物 刷新过的图片 [BJDCTF2020]认真你就输了 [BJDCTF2020]藏藏藏

被劫持的神秘礼物 下载文件 提示让我们找账号密码 wireshark打开上述文件 可以发现一个POST请求登录接口的HTTP包&#xff0c;追踪http流 数据包中可以发现用户名&#xff1a;admina 密码&#xff1a;adminb 打开md5在线加密 得到flag flag{1d240aafe2…

第二证券|新能源优势突出 青海加速储能产业布局

大唐青海动力开发有限公司工作人员在青海省海南藏族自治州共和县塔拉滩光伏电站巡检。 无论是新动力场站直流侧的储能技能应用&#xff0c;还是同享储能形式试点&#xff0c;近年来&#xff0c;青海储能职业迎来跨越式展开新阶段。业内遍及看好未来储能展开&#xff0c;作为全…

多线程(2)

文章目录前言 &#xff1a;1.Thread类 &#xff1a;1.1 Thread类常见的构造方法1.2 Thread的几个常见属性1.3 中断一个线程1.4 等待一个线程-join()1.5 获取当前线程引用1.6 休眠当前线程2.线程状态前言 &#xff1a; 简单回顾上文知识点 上文我们了解了 线程是为解决并发编程引…

Linux网络编程——IO多路复用

文章目录1&#xff0c;I/O模型2&#xff0c;阻塞I/O 模式2.1&#xff0c;读阻塞&#xff08;以read函数为例&#xff09;2.2&#xff0c;写阻塞3&#xff0c;非阻塞I/O模式3.1&#xff0c;非阻塞I/O模式的实现&#xff08;fcntl()函数、ioctl() 函数&#xff09;3.1.1&#xff…