一篇了解分布式id生成方案

news2024/11/29 10:46:09

系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会有多个ID生成的策略。下面就介绍一些常见的ID生成策略。

1.数据库自增长序列或字段

最常见的方式。利用数据库,全数据库唯一

关系型数据库都实现数据库自增ID;mysql通过auto_increment实现、oralce通过sequence实现。
在数据库集群环境下,不同数据库节点可设置不同起步值、相同步长值来实现集群下生成全局唯一、递增ID

优点:

1)简单,代码方便,性能可以接受。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
3)在性能达不到要求的情况下,比较难于扩展。
4)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。
5)分表分库的时候会有麻烦。

2.UUID

常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一。

140a5382-c69a-4c14-84e8-07c95a9bae2b

优点:

1)简单,代码方便。
2)生成ID性能非常好,基本不会有性能问题。
3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

缺点:

1)没有排序,无法保证趋势递增。
2)UUID往往是使用字符串存储,查询的效率比较低。
3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
4)传输数据量大
5)不可读。不方便操作!

3.Redis生成ID

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCRINCRBY来实现。

可以使用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

这个,随便负载到哪个机确定好,未来很难做修改。但是3-5台服务器基本能够满足器上,都可以获得不同的ID。但是步长和初始值一定需要事先需要了。使用Redis集群也可以方式单点故障的问题。

另外,比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

优点:

1)不依赖于数据库,灵活方便,且性能优于数据库。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
2)需要编码和配置的工作量比较大。

4.Twitter的snowflake算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。具体实现的代码可以参看https://github.com/twitter/snowflake

package com.easygo.utils;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
 * <p>名称:IdWorker.java</p>
 * <p>描述:分布式自增长ID</p>
 * <pre>
 *     Twitter的 Snowflake JAVA实现方案
 * </pre>
 * 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
 * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
 * 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
 * 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
 * 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
 * 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
 * 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
 * <p>
 * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
 *
 * @author Polim
 */
public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    private final static long twepoch = 1288834974657L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        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));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

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

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

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
            /*
             * GET jvmPid
             */
            mpid.append(name.split("@")[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }

    public static void main(String[] args) {
        for (int i = 0; i <1000; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            IdWorker worker=new IdWorker(0, i);
            long num = worker.nextId();
            System.out.println(num);
        }
    }
}

snowflake算法可以根据自身项目的需要进行一定的修改。比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的bit数。

优点:

1)不依赖于数据库,灵活方便,且性能优于数据库。
2)ID按照时间在单机上是递增的。

缺点:

1)在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。

5.利用zookeeper生成唯一ID

zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。

很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想。

https://blog.csdn.net/wuliu_forever/article/details/53389483

6.MongoDB的ObjectId

MongoDB的ObjectId和snowflake算法类似。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB 从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求。使其在分片环境中要容易生成得多。

其格式如下:
在这里插入图片描述
前4 个字节是从标准纪元开始的时间戳,单位为秒。时间戳,与随后的5 个字节组合起来,提供了秒级别的唯一性。由于时间戳在前,这意味着ObjectId 大致会按照插入的顺序排列。这对于某些方面很有用,如将其作为索引提高效率。这4 个字节也隐含了文档创建的时间。绝大多数客户端类库都会公开一个方法从ObjectId 获取这个信息。
接下来的3 字节是所在主机的唯一标识符。通常是机器主机名的散列值。这样就可以确保不同主机生成不同的ObjectId,不产生冲突。
为了确保在同一台机器上并发的多个进程产生的ObjectId 是唯一的,接下来的两字节来自产生ObjectId 的进程标识符(PID)。
前9 字节保证了同一秒钟不同机器不同进程产生的ObjectId 是唯一的。后3 字节就是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId 也是不一样的。同一秒钟最多允许每个进程拥有2563(16 777 216)个不同的ObjectId。

实现的源码可以到MongoDB官方网站下载。

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

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

相关文章

BabylonJS之放烟花

BabylonJS烟花效果视频一&#xff1a; 技术调研 1. 方案一&#xff1a;ParticleSystem 用ParticleSystem来实现每一束的烟花效果&#xff0c;如果浏览器支持WebGL2功能&#xff0c;使用GPUParticleSystem性能会有极大的提升。 优点&#xff1a; 烟花效果易实现且效果好。 缺点…

什么是品牌营销?学会正确推广您的业务

什么是品牌营销&#xff1f; 品牌营销涉及长期战略规划&#xff0c;以推广整个品牌&#xff0c;而不是营销单个产品或服务。它分享了一个引人入胜的故事&#xff0c;以在潜在客户中产生品牌知名度并建立声誉。 面向消费者的品牌使用品牌智能软件来了解人们对其品牌的看法&#…

磨金石教育摄影技能干货分享|极简艺术与人文摄影相结合(一)

柏林街头&#xff08;德国&#xff09;照片中无论是景物元素还是色彩元素都很少&#xff0c;主体人物与道路外加一个箱子。极简艺术照片的减法做到了。一名女子端坐在路边&#xff0c;白色的衣服一尘不染&#xff0c;手持镜子补妆。虽然时间场地比较的随意&#xff0c;但是依然…

R统计绘图 | 物种组成冲积图(绝对/相对丰度,ggalluvial)

一、数据准备 数据使用的不同处理土壤样品的微生物组成数据&#xff0c;包含物种丰度&#xff0c;分类单元和样本分组数据。此数据为虚构&#xff0c;可用于练习&#xff0c;请不要作他用。 # 1.1 设置工作路径 #knitr::opts_knit$set(root.dir"D:\\EnvStat\\PCA")#…

A/B 测试成为企业“新窗口”:增长盈利告别经验主义,数据科学才是未来

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 如何能够预知一个产品的未来&#xff1f;最好的办法当然是穿越到未来看一看。 这种“模拟未来、窥探底牌”的设想似乎只是一种天方夜谭。尤其在数字化浪潮冲击下&a…

磨金石教育摄影技能干货分享|杨元惺佳作欣赏——诗意人文

一般来说&#xff0c;人文摄影总会体现现实性多些。但杨老师是个摄影诗人&#xff0c;他的内心总能将刻板的现实融入美好的光芒。你在他的照片里&#xff0c;看着现实的摄影素材&#xff0c;所感受到的是诗意的绵绵未尽。春网&#xff08;中国&#xff09;正所谓春水碧于天&…

Word控件Spire.Doc 【Table】教程(19):在 C# 中的 Word 中添加/获取表格的替代文本

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

7、STM32 FSMC驱动SRAM

本次使用CubeMx配置FSMC驱动SRAM,XM8A51216 IS62WV51216 原理图&#xff1a; 注意&#xff1a;FSMC_A0必须对应外部设备A0引脚 一、FSMC和FMC区别 FSMC&#xff1a;灵活的静态存储控制器 FMC:灵活存储控制器 区别&#xff1a;FSMC只能驱动静态存储控制器&#xff08;如&…

软考中级有用吗

当然有用了&#xff01; 软考“简历”&#xff1a;计算机软件资格考试在全国范围内已经实施了二十多年&#xff0c;近十年来,考试规模持续增长&#xff0c;截止目前,累计报考人数约有五百万人。该考试由于其权威性和严肃性&#xff0c;得到了社会各界及用人单位的广泛认同&…

哈希函数的学习算法整理

前言 如果你对这篇文章可感兴趣&#xff0c;可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」&#xff0c;查看完整博客分类与对应链接。 概述 哈希函数学习的两个步骤&#xff1a; 转为二进制编码&#xff1a;可以先降维成实数&#xff0c;再转为二进制&…

【Spark分布式内存计算框架——离线综合实战】4. IP 工具类

2.2 IP 工具类 需要将IP地址代码封装到工具类中&#xff0c;方便后续使用&#xff0c;在包【cn.itcast.spark.utils】创建工具类&#xff1a;IpUtils.scala&#xff0c;定义方法【convertIpToRegion】&#xff0c;传递参数【ip地址和DbSearch对象】&#xff0c;返回Region对象…

数据结构-树的理解

目录 一&#xff1a;要解决的问题&#xff0c;出发点 1.演进 树的定义&#xff1a; 树的深度&#xff08;高度&#xff09; 平衡二叉树&#xff08;AVL树&#xff09; 红黑树&#xff1a; B树&#xff1a; 深夜有感&#xff0c;灵感乍现&#xff0c;忽然感觉对这个数据结…

Unity(二)--通过简单例子了解UGUI几个常用对象操作(Text,Image,Button)

目录 文本框等UI对象的添加Canvas 画布给Canvas添加脚本,绑定要操作的对象文本框Text的使用图像Image的使用更换图片Type:显示图片相关按钮Button的使用过渡导航事件绑定文本框等UI对象的添加 Canvas 画布 当创建一个UI元素的时候,如果没有Canvas 画布,就会自动创建一个画布…

学习资料|SSH隧道端口转发功能详解

概念ssh隧道大致可以分为3种&#xff0c;分别为本地端口转发&#xff0c;远程端口转发&#xff0c;动态端口转发&#xff0c;本文将让你彻底搞懂这3个转发的命令表达形式&#xff0c;让你能够灵活运用解决生活中的各种特殊场景。如果你正在使用mobaxterm、xshell、secureCRT、p…

怎样深度学习?主题碾压式学习法

怎样最深度的学习&#xff1f;【主题碾压式&#xff01;】 对一个学习主题&#xff0c;大体量投入学习资源 进行对比和实践 会取得突破 限定在社会科学和社会应用范围 趣讲大白话&#xff1a;大力出奇迹 【趣讲信息科技&#xff1a;84期&#xff0c;下期预告&#xff1a;很少有…

C++条件变量唤醒问题 notify_one() 唤醒不及时问题

条件变量唤醒问题 & notify_one() 唤醒不及时问题 因为我对于 C中条件变量的等待唤醒部分、notify_all & notify_one 的区别方面有些疑点&#xff0c;因此就有了以下的同 chatgpt 的沟通&#xff0c;希望同样能够帮助到大家 感叹于 chatgpt的强大 问题&#xff1f; 我比…

(三十二)大白话MySQL一起来看看INSRET语句的undo log回滚日志长什么样?

昨天我们讲解了undo log回滚日志的作用&#xff0c;说白了&#xff0c;就是你执行事务的时候&#xff0c;里面很多INSERT、UPDATE和DELETE语句都在更新缓存页里的数据&#xff0c;但是万一事务回滚&#xff0c;你必须有每条SQL语句对应的undo log回滚日志&#xff0c;根据回滚日…

Docker 名词介绍

Docker核心名词镜像文件镜像:简单理解为就是一个安装包&#xff0c;里面包含容器所需要运行的的基础文件和配置信息&#xff0c;比如&#xff1a;redis镜像、mysql镜像等。镜像的来源方式&#xff1a;1. 自己做镜像 比如&#xff08;自己开发微服务项目&#xff09;2. 拉取别人…

python学习笔记——数据类型总结

1.基本数据类型 &#xfeff; 数据类型对应的内置函数&#xff1a;将其他类型&#xff0c;转换成自己的类型。 int()float()bool()str()list()tuple()set()dict() 2.数据类型对比 &#xfeff;&#xfeff; 3.列表 w [a,b,c] #查 print(w[0]) print(w[0:3:2]) #增 w.appe…

css 属性和属性值的定义

文章目录css文本属性作业列表属性背景属性作业css文本属性 序号属性描述说明1font-size字体大小浏览器默认16px&#xff1b;2font-family字体当字体是中文字体&#xff0c;英文字体&#xff0c;中间有空格时候&#xff0c;要加双引号&#xff0c;多字体之间用逗号隔开 默认微软…