分布式Id生成策略-美团Leaf

news2024/11/13 16:36:34

之前在做物流相关的项目时候,需要在分布式系统生成运单的id。

1.需求:

1.全局唯一性:不能出现重复的ID。(基本要求)

2.递增:大多数关系型数据库(如 MySQL)使用 B+ 树作为索引结构。如果 ID 是递增的,新数据总是追加到索引的末尾,这样索引的维护成本较低,因为数据库不需要频繁调整树的结构。相反,如果 ID 是随机的,数据插入时可能需要频繁调整索引结构,导致写入性能下降。

3.业务性:对具体的场景的ID要具备业务的特性,比如顺丰运单ID为类似SF1000000000760

4.精简性:某些场景下的ID不宜过长,所以对位数/长度有所限制。

在分布式系统中我们还应该考虑:生成方案能子啊多节点正常工作,能够有一定的解决故障问题,有高可用性,生成ID的速率要快,能够随业务扩展进行水平拓展,比如在分库分表后也能兼容原来的ID。

2.方案

有几个方案可以考虑:

本地的UUID生成:它的优点的生成速度很快,产生的值也几乎达到不重复的要求,但是产生的ID值比较长,可以达到36个字符,可读性还非常差,而且ID完全随机,没有任何的顺序。因此这个方案不考虑。

依靠数据库自增特性:为不同业务模块建立一张自增表维护递增序列。这种方法比较简单,靠数据库保证自增机制。缺点也很明显,当数据库异常整个系统不可用,而且ID生成的性能瓶颈也限制在单台MYSQL数据库。单台数据库性能问题可以同过部署多态机器,每个机器设置不同初始值,并且步长和机器数相同来优化。但是也带来很多问题,比如扩容很是麻烦。

Redis实现:通过给不同业务设计不同的key,通过INCR命令,对key自增,得到全局唯一并有序的ID。Redis每秒支持10W的读写,所以性能问题得到解决,但是Redis依靠内存,虽然有持久化机制,但是它持久化是先写内存再异步刷盘,遇到没来得及持久化就宕机也是会出大问题的。

雪花算法: 是 Twitter 于 2010 年开源的一种分布式唯一 ID 生成算法,它可以在分布式系统中生成高效、有序的唯一 ID。雪花算法生成的 ID 是一个 64 位的长整型long),在保证唯一性的同时,也确保了生成的 ID 按时间顺序递增。

在这里插入图片描述

  1. 全局唯一性:生成的 ID 保证全局唯一,雪花算法结合时间戳、机器 ID 和序列号确保在分布式系统中不会产生重复的 ID。

  2. 高效生成:雪花算法不依赖数据库,因此生成 ID 的过程非常高效,可以在本地的内存中生成,具有极高的性能。每台机器每秒钟可以生成上百万个 ID。

  3. 趋势递增:生成的 ID 是按时间顺序递增的,尤其是基于时间戳的部分,使得 ID 具有递增的特性。这对数据库插入数据时索引的维护非常友好(例如 B+ 树结构索引的维护成本较低)。

  4. 灵活可扩展:通过调整数据中心 ID 和机器 ID 的位数分配,可以根据业务的需要适当扩大集群规模或提升单机 ID 生成的并发能力。

    雪花算法依赖 机器码 来保证不同机器生成的 ID 唯一性。如果在分布式环境中多台机器未能准确区分它们的机器码,可能导致多个机器在同一时间生成相同的 ID,造成 ID 冲突。因此,在分布式系统中,每台服务器、虚拟机或容器必须手动指定一个唯一的机器标识符。**

雪花算法的不足

  • 依赖机器时间:由于 ID 的递增性依赖时间戳,一旦服务器的系统时钟发生回拨,可能会引发 ID 冲突或无法生成 ID 的问题。虽然有一些解决方案(如等待或借助其他算法生成 ID),但还是可能影响生成 ID 的稳定性。

3.美团Leaf

下面就将引入我选取的美团Leaf这个id生成策略。

其源码托管于GitHub:https://github.com/Meituan-Dianping/Leaf

这里有个美团的技术播客,专门介绍了Leaf:https://tech.meituan.com/2017/04/21/mt-leaf.html

目前Leaf覆盖了美团点评公司内部金融、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。在4C8G VM基础上,通过公司RPC方式调用,QPS压测结果近5w/s,TP999 1ms。

Leaf 提供两种生成的ID的方式(segment模式和snowflake模式),我们采用segment模式(号段)来生成运单号。

号段模式

号段模式采用的是基于MySQL数据生成id的,它并不是基于MySQL表中的自增长实现的,因为基于MySQL的自增长方案对于数据库的依赖太大了,性能不好,Leaf的号段模式是基于一张表来实现,每次获取一个号段,生成id时从内存中自增长,当号段用完后再去更新数据库表,如下:

在这里插入图片描述

字段说明:

  • biz_tag:业务标签,用来区分业务
  • max_id:表示该biz_tag目前所被分配的ID号段的最大值
  • step:表示每次分配的号段长度。如果把step设置得足够大,比如1000,那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step
  • description:描述
  • update_time:更新时间

架构图如下:

在这里插入图片描述

图片来源:https://tech.meituan.com/2017/04/21/mt-leaf.html

说明:test_tag在**第一台Leaf机器上是11000的号段**,当这个号段用完时,会去加载另一个长度为step=1000的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是30014000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000,更新号段的SQL语句如下:

Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit

Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。

双buffer优化

Leaf为此做了优化,增加了双buffer优化。

当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。
在这里插入图片描述

双buffer原理,来自:https://tech.meituan.com/2017/04/21/mt-leaf.html

采用双buffer的方式,**Leaf服务内部有两个号段缓存区segmen。**当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS(秒处理事务数)的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

4.项目使用

我们只用到了号段的方式,并没有使用雪花方式,所以只需要创建数据库表即可

将其镜像运行:

docker run \
-d \
-v /hujx/meituan-leaf/leaf.properties:/app/conf/leaf.properties \
--name meituan-leaf \
-p 28838:8080 \
--restart=always \
registry.cn-hangzhou.aliyuncs.com/itheima/meituan-leaf:1.0.1

leaf.properties

leaf.name=leaf-server
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://192.168.150.101:3306/hjx_leaf?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
leaf.jdbc.username=root
leaf.jdbc.password=123

leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=

创建sl_leaf数据库脚本:

CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
  `max_id` bigint NOT NULL DEFAULT '1',
  `step` int NOT NULL,
  `description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- 插入运单号生成规划数据
INSERT INTO `leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`, `update_time`) VALUES ('transport_order', 1000000000001, 100, 'Test leaf Segment Mode Get Id', '2023-07-07 11:32:16');

封装服务

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.sl.transport.common.enums.IdEnum;
import com.sl.transport.common.exception.SLException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

/**
 * id服务,用于生成自定义的id
 */
@Service
public class IdService {

    @Value("${sl.id.leaf:}")
    private String leafUrl;

    /**
     * 生成自定义id
     *
     * @param idEnum id配置
     * @return id值
     */
    public String getId(IdEnum idEnum) {
        String idStr = this.doGet(idEnum);
        return idEnum.getPrefix() + idStr;
    }

    private String doGet(IdEnum idEnum) {
        if (StrUtil.isEmpty(this.leafUrl)) {
            throw new SLException("生成id,sl.id.leaf配置不能为空.");
        }
        //访问leaf服务获取id
        String url = StrUtil.format("{}/api/{}/get/{}", this.leafUrl, idEnum.getType(), idEnum.getBiz());
        //设置超时时间为10s
        HttpResponse httpResponse = HttpRequest.get(url)
                .setReadTimeout(10000)
                .execute();
        if (httpResponse.isOk()) {
            return httpResponse.body();
        }
        throw new SLException(StrUtil.format("访问leaf服务出错,leafUrl = {}, idEnum = {}", this.leafUrl, idEnum));
    }

}

public enum IdEnum implements BaseEnum {

    TRANSPORT_ORDER(1, "运单号", "transport_order", "segment", "SL");

    private Integer code;
    private String value;
    private String biz; //业务名称
    private String type; //类型:自增长(segment),雪花id(snowflake)
    private String prefix;//id前缀

    IdEnum(Integer code, String value, String biz, String type, String prefix) {
        this.code = code;
        this.value = value;
        this.biz = biz;
        this.type = type;
        this.prefix = prefix;
    }

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getValue() {
        return this.value;
    }

    public String getBiz() {
        return biz;
    }

    public String getType() {
        return type;
    }

    public String getPrefix() {
        return prefix;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("IdEnum{");
        sb.append("code=").append(code);
        sb.append(", value='").append(value).append('\'');
        sb.append(", biz='").append(biz).append('\'');
        sb.append(", type='").append(type).append('\'');
        sb.append(", prefix='").append(prefix).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

使用步骤:

  • 在配置文件中进行配置sl.id.leaf为: 地址:你的服务端口 如:http://192.168.150.101:28838
    pend(“, type='”).append(type).append(‘’‘);
    sb.append(", prefix=’").append(prefix).append(‘’‘);
    sb.append(’}');
    return sb.toString();
    }
    }

使用步骤:

  • 在配置文件中进行配置sl.id.leaf为: 地址:你的服务端口 如:http://192.168.150.101:28838
  • 在Service中注入IdService,调用getId()方法即可,例如:idService.getId(IdEnum.TRANSPORT_ORDER)

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

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

相关文章

web前端-HTML常用标签-综合案例

如图&#xff1a; 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document&…

mysql时间戳格式化yyyy-mm-dd

格式化到 年月日 # 将时间换成列名就行&#xff1b;当前是秒级时间戳&#xff0c;如果是毫秒的 / 1000即可 # SELECT FROM_UNIXTIME(1602668106666.777888999 / 1000,%Y-%m-%d) AS a; # SELECT FROM_UNIXTIME(列名 / 1000,%Y-%m-%d) AS a; SELECT FROM_UNIXTIME(1602668106.666…

Linux 系统进程理解——标识符,状态

目录 进程描述-pcb 并行与并发 概念&#xff1a; 课本概念&#xff1a;程序的一个执行实例&#xff0c;正在执行的程序等 内核观点&#xff1a;担当分配系统资源&#xff08;CPU时间&#xff0c;内存&#xff09;的实体 这短短的两行就概括了进程&#xff0c;但是进程的内在…

Mysql分组取最新一条记录

文章目录 Mysql分组取最新一条记录1. 数据准备1. 方法1&#xff1a;使用子查询获取每个组的最大时间戳&#xff0c;然后再次查询获取具体记录&#xff08;如果时间戳是唯一的&#xff09;2. 方法2&#xff1a;使用窗口函数&#xff08;MySQL 8.0&#xff09;3. 方法3&#xff1…

ClickHouse 与 Quickwit 集成实现高效查询

1. 概述 在当今大数据分析领域&#xff0c;ClickHouse 作为一款高性能的列式数据库&#xff0c;以其出色的查询速度和对大规模数据的处理能力&#xff0c;广泛应用于在线分析处理 (OLAP) 场景。ClickHouse 的列式存储和并行计算能力使得它在处理结构化数据查询时极具优势&…

F28335 的外部中断实验

1 外部中断介绍 1.1 外部中断简介 1.2 外部中断相关寄存器 (1)外部中断控制寄存器(XINTnCR) (2)外部 NMI 中断控制寄存器

多模态大模型MiniCPM-V技术学习

目前性价比最高的多模态模型 Minicpm-V-2.6参数8B&#xff0c;int4版本推理显存仅7GB&#xff0c;并且在幻觉数据集上效果好于其他模型&#xff0c;测试下来效果非常好&#xff0c;官方演示里面还给出了手机上端侧运行的图片和视频推理示例 p.s.Qwen2-VL和Minicpm-V-2.6头对头…

从小白到大神:C语言预处理与编译环境的完美指南(上)

从小白到大神&#xff1a;C语言预处理与编译环境的完美指南&#xff08;下&#xff09;-CSDN博客 新鲜出炉~~&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;下篇在这里&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&…

echarts图表刷新

图表制作完成&#xff0c;点击刷新图标&#xff0c;可以刷新。 <div class"full"><div id"funnel" class"normal"></div><div class"refreshs"><div class"titles_pic"><img src"./…

数据飞轮崛起:数据中台真的过时了吗?

一、数据中台的兴起与困境 随着大数据技术的不断发展&#xff0c;我见证了企业数据能力建设的演变。从数据中台的兴起&#xff0c;到如今数据飞轮模式的热议&#xff0c;企业的数据管理理念经历了巨大的变化。起初&#xff0c;数据中台作为解决数据孤岛、打破部门壁垒的“救星…

创新引领未来,Vatee万腾平台助力企业飞跃发展

在日新月异的科技浪潮中&#xff0c;创新已成为推动社会进步和企业发展的核心动力。Vatee万腾平台&#xff0c;作为数字化转型领域的佼佼者&#xff0c;正以其独特的创新理念和强大的技术实力&#xff0c;引领着企业迈向更加辉煌的未来&#xff0c;助力企业实现飞跃式发展。 创…

如何将很多个pdf拼接在一起?很多种PDF拼接的方法

如何将很多个pdf拼接在一起&#xff1f;将多个PDF文件合并不仅能够提升信息的整合性&#xff0c;还能使文件管理更加高效。想象一下&#xff0c;你需要向同事或老师提交一份综合报告&#xff0c;其中包含了多份相关资料。如果每个文件单独存在&#xff0c;查找和传输都会变得繁…

Redis中Hash(哈希)类型的基本操作

文章目录 一、 哈希简介二、常用命令hsethgethexistshdelhkeyshvalshgetallhmgethlenhsetnxhincrbyhincrbyfloathstrlen 三、命令小结四、哈希内部编码方式五、典型应用场景六、 字符串&#xff0c;序列化&#xff0c;哈希对比 一、 哈希简介 几乎所有的主流编程语言都提供了哈…

(蓝桥杯)STM32G431RBT6(TIM4-PWM)

一、基础配置 这个auto-reload preload是自动重装载值&#xff0c;因为我们想让他每改变一个占空比&#xff0c;至少出现一次周期 Counter Period(Autoreload Regisiter)这个设值为10000&#xff0c;那么就相当于它的周期是10000 脉冲宽度可以设置为占周期的一半&#xff0c;那…

docker部署excalidraw画图工具

0&#xff09;效果 0.1&#xff09;实时协作 0.2&#xff09;导出格式 1&#xff09;docker安装 docker脚本 bash <(curl -sSL https://cdn.jsdelivr.net/gh/SuperManito/LinuxMirrorsmain/DockerInstallation.sh)docker-compose脚本 curl -L "https://github.com/…

【随手笔记】使用J-LINK读写芯片内存数据

第一种使用JLINK.exe 1. 打开j-link.exe 2.输入【usb】 3. 连接芯片 输入【connect】输入芯片型号【STM32L071RB】输入连接方式 【S】 使用SWD连接方式输入连接速率 【4000】连接成功 4. 输入【&#xff1f;】查看指令提示 5. 读写指令 Mem Mem [<Zone>…

Redis的主从模式、哨兵模式、集群模式

最近学习了一下这三种架构模式&#xff0c;这里记录一下&#xff0c;仅供参考 目录 一、主从架构 1、搭建方式 2、同步原理 3、优化策略&#xff1a; 4、总结&#xff1a; 二、哨兵架构 1、搭建哨兵集群 2、RedisTemplate如何使用哨兵模式 三、分片集群架构 1&#…

JVM面试题-说一下JVM主要组成部分及其作用

总体来说&#xff0c;方法区和堆是所有线程共享的内存区域&#xff1b;而虚拟机栈、本地方法栈和程序计数器的运行是线程私有的内存区域&#xff0c;运行时数据区域就是我们常说的JVM的内存。 类加载子系统&#xff1a;根据给定的全限定名类名(如&#xff1a;java.lang.Object…

【D3.js in Action 3 精译_024】3.4 让 D3 数据适应屏幕(上)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理

文章目录 一、AJAX二、HTTP协议1. 请求报文2. 响应报文 三、AJAX案例准备1. 安装node2. Express搭建服务器3. 安装nodemon实现自动重启 四、AJAX发送请求1. GET请求2. POST请求(1) 配置请求体(2) 配置请求头 3. 响应JSON数据的两种方式(1) 手动&#xff0c;JSON.parse()(2) 设置…