每台电脑的唯一标识_聊聊分布式唯一ID

唯一ID在分布式系统中至关重要,确保不重复且能精确识别对象。文章探讨了MySQL自增ID、Redis生成ID、UUID和Twitter的Snowflake算法等生成唯一ID的方法,分析了各自的优缺点和适用场景。MySQL的ID有序但有性能瓶颈,Redis生成快速但可能有单点故障,UUID无序但本地生成,Snowflake算法则兼顾性能和有序性。
摘要由CSDN通过智能技术生成

为什么需要唯一ID

「ID」—又称「标识符」(identifier),是一个可以唯一识别一个对象或者物体的名称,被识别的对象可能是一些想法、物理上可数的对象或者物理上的不可数物质。在传统单机服务的情况下,往往通过一个数据库的逐渐甚至内存中生成的数据就可以唯一标识一个用户或者订单,但是随着业务的增长,出现多台主机和多个数据库分库分表,也就是所谓的分布式服务出现,这时候就需要一个不会重复的全局唯一ID来避免碰撞。

分布式唯一ID的要求

  1. 「唯一性」:最重要的要求,唯一标识符「必须不重复」。假如我们通过支付软件进行一笔转账,在系统后台会生成一个业务ID,如果这次转账出现了网络超时,那么如何判定这次转账是否成功呢?这时候唯一ID就起到判定作用,通过后台系统的查找,如果接收方已经接受了这个业务ID,就返回成功,否则返回失败,这样就可以避免类似二次转账这样的事情发生。很多业务场景和系统底层都会用到标识符的「唯一性」,唯一性为我们带来的就是精确识别对象的能力。
  2. 「无意性」:标识符应该不包含「重要的意义」,具体来说就是不应该包含和具体场景和业务相关的内容,注意无意性「不作为必须要求」,可以包含这些信息,但是由于标识符长度一般都是固定的,所以会造成唯一性减弱,并且有意义的标识符也可能带来数据安全问题。举一个例子,身份证号作为我们的身份标识符,其中的大多数符号都是有重要意义的:6位地区+8位出生年月+3位ID+1位校验码,可以发现可灵活变化的位数较少,只有3位,如果同一天同一个地区有超过1000个新生儿,那么就会出现编码重复的问题。也许在生活中出现这样的可能性较低,但是在互联网系统中这样的并发情况每时每刻都可能会发生,所以往往避免在有限的标识符中减少有意义的字符来提升标识符的唯一性。
  3. 「有序性」:标识符和标识符之间如果能够具备有序性,那么将大大提升查找效率。注意有序性也「不是必须要求」,标识符的作用就是精确定位对象,而在实际业务中对应的最常用的操作也就是查询。例如下面会提到的MySQL索引的基本原理是B+树,而有序性的递增数字可以大大提升查找效率。

分布式唯一ID生成

下面从最简单的MySQL开始谈谈几种主流的分布式唯一ID生成方案。

一、MySQL生成ID

MySQL是一种关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中。

MySQL作为数据库界的老大哥,早在古早因特网时期,其数据主键就采用了自增(auto_increment)的策略来进行唯一ID的生成。

优缺点
  • 优点:数据库生成的ID绝对有序,高可用实现方式简单。

  • 缺点:需要独立部署数据库实例,成本高,有性能瓶颈。

优化方案

这个方案在早期互联网时代是简单而有效的而且绝对有序的特点让数据库查询变得十分高效,但是随着发展,人们渐渐发现这样的方案越来越支撑不住高并发的场景,单个数据库的性能瓶颈决定了注定需要做出改变,所以有了两种优化方案。

  1. 多个数据库设定不同的初始值和步长:

    优点:分散单个数据库压力,提高并发能力

    缺点:数据库扩容困难

    如下图所示,使用多台数据库分别设置不同步长,生成不重复ID的策略可保证每台数据库生成的ID是不冲突的,这样充分缓解了单台服务器存在的并发压力,但这种固定步长的方式也会带来「扩容的问题」,很容易想到当扩容时会出现无ID初始值可分的问题。

    35a3ee7f76c86c7f8c8333a17e8133e2.png

  2. 单个服务批量生成一定数量的ID:

    优点:避免了每次生成ID都要访问数据库并带来压力,提高性能

    缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续

    如下图所示,由独立服务一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中记录当前值及最大值。存在问题是初始值最大值存放在内存中,如果中途故障重启,那么可能会导致ID连续性被破坏。

    b825d9edd92ef3425d41d179b690efc9.png

二、Redis生成ID

Redis是一种非关系型数据库,也是缓存数据库,即将数据存储在缓存中,缓存的读取速度快。

Redis的所有命令操作都是单线程的,本身提供自增原子命令,所以能保证生成的ID肯定是唯一有序的。

优缺点
优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排序的结果很有帮助。缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作量比较大。
三、UUID

「UUID」—通用唯一标识符(universally unique identifier)是一种软件建构的标准,用于标识计算机系统中的信息。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。目前最广泛应用的UUID,是微软公司的全局唯一标识符(GUID)。

组成

UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。按照开放软件基金会(OSF)制定的标准计算,用到了「以太网卡地址、纳秒级时间、芯片ID码和随机数」

UUID由以下几部分的组合:

  • 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。

  • 时钟序列。

  • 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。

版本

UUID具有多个版本,每个版本的算法不同,应用范围也不同。

首先是一个特例—「Nil UUID」,通常我们不会用到它,它是由全为0的数字组成:00000000-0000-0000-0000-000000000000

  1. 基于时间的UUID

    基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)。

  2. DCE安全的UUID

    DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。

  3. 基于名字的UUID(MD5)

    基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。

  4. 随机UUID

    根据随机数,或者伪随机数生成UUID。这种UUID产生重复的概率是可以计算出来的,但随机的东西就像是买彩票:你指望它发财是不可能的。

  5. 基于名字的UUID(SHA1)

    和基于名字的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

文末有一个UUID生成网站,可以实践尝试一下,有助于更好地理解。

部分版本伪码

从UUID生成网站也可以发现,在众多版本中,用到较多的是基于时间的版本1与基于随机数的版本5,在这里也给出这两个版本的实现伪码,了解一下大致的生成原理。

// 版本 1 - 基于时间的UUID:
gen_uuid() {
    struct uuid uu;

    // 获取时间戳
    get_time(&clock_mid, &uu.time_low);
    uu.time_mid = (uint16_t) clock_mid; // 时间中间位
    uu.time_hi_and_version = ((clock_mid >> 16) & 0x0FFF) | 0x1000; // 时间高位 & 版本号

    // 获取时钟序列。在libuuid中,尝试取时钟序列+1,取不到则随机;在python中直接使用随机
    get_clock(&uu.clock_seq);// 时钟序列+1 或 随机数
    uu.clock_seq |= 0x8000;// 时钟序列位 & 变体值

    // 节点值
    char node_id[6];
    get_node_id(node_id);// 根据mac地址等获取节点id
    uu.node = node_id;
    return uu;
}

// 版本4 - 基于随机数的UUID:
gen_uuid() {
    struct uuid uu;
    uuid_t buf;

    random_get_bytes(buf, sizeof(buf));// 获取随机出来的uuid,如libuuid根据进程id、当日时间戳等进行srand随机

    uu.clock_seq = (uu.clock_seq & 0x3FFF) | 0x8000;// 变体值覆盖
    uu.time_hi_and_version = (uu.time_hi_and_version & 0x0FFF) | 0x4000;// 版本号覆盖
    return uu;
}

优缺点
  • 优点:本地生成,生成简单,性能好,没有高可用风险

  • 缺点:长度过长,存储冗余,且无序不可读,查询效率低

四、Twitter的snowflake算法

Twitter 利用 zookeeper 实现了一个全局ID生成算法 Snowflake

组成

84e6c818b3641698ddf5a12b9928cf6b.png

如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:

  • 「1位符号位」

由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用的ID一般都是正数,所以最高位为 0。

  • 「41位时间戳(毫秒级)」

需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年

  • 「10位数据机器位」

包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024 s个节点。超过这个数量,生成的ID就有可能会冲突。

  • 「12位毫秒内的序列」

这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID

加起来刚好64位,为一个Long型。

优缺点
  • 优点:高性能,低延迟,按时间有序,一般不会造成ID碰撞
  • 缺点:需要独立的开发和部署,依赖于机器的时钟,如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序

分布式唯一ID小结

通过刚刚介绍的几种ID生成方案,除了数据库自生成ID之外,常用的分布式唯一ID生成思路基本是利用一个长串数字或字符串,将其分割成多个部分,分别记录时间信息、机器/名字信息、随机信息、序列信息等。时间信息部分决定了该策略能使用的时长,机器/名字信息支持了分布式环境下的独自生成唯一ID与识别能力,序列信息保证了事件的顺序记录以及同一时间单位下的并发数,而随机信息则加大了ID整体的不可识别性。实际上如果现有的方法依然不能满足,我们完全可以依据自身业务和发展需求,来自行决定使用何种策略生成唯一ID。

下面对几个比较流行的方案做个总结对比:

方案唯一性有序性可用性自主性安全性冲突率
基于时间的UUID强唯一性时间序+逻辑序高并发可用自主生成暴露时间及MAC10M/s 下不冲突
基于随机值的UUID依赖随机算法无序依赖随机算法自主生成安全依赖随机算法
数据库自增ID强唯一性有序较高可用依赖中心主机暴露数量不冲突
数据库批量ID强唯一性批量内有序较高可用依赖中心主机暴露数量不冲突
snowflake算法较强唯一性时间序+逻辑序高并发可用自主生成暴露时间400W/s(取决于序列号位数)

注:部分图文来自互联网

相关链接:

  • UUID维基百科:

    https://en.wikipedia.org/wiki/Universally_unique_identifier

  • UUID生成网站:https://www.uuidgenerator.net/

b5fbf94cafccf649c90a652d355f374a.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值