极客时间云原生训练营--百度网盘下载--分布式系统唯一ID生成策略总结

微信642620018,获取极客时间云原生训练营完整版

全局唯一 id 介绍

    系统唯一 id 是我们在设计阶段常常遇到的问题。在复杂的分布式系统中,几乎都需要对大量的数据和消息进行唯一标识。在设计初期,我们需要考虑日后数据量的级别,如果可能会对数据进行分库分表,那么就需要有一个全局唯一 id 来标识一条数据或记录。生成唯一 id 的策略有多种,但是每种策略都有它的适用场景、优点以及局限性。

全局唯一 id 特点:

全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求;

趋势递增:在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能;

单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求;

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

高可用性:同时除了对 ID 号码自身的要求,业务还对 ID 号生成系统的可用性要求极高,想象一下,如果 ID 生成系统瘫痪,这就会带来一场灾难。所以不能有单点故障;

分片支持:可以控制 ShardingId。比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易;

长度适中。

常见全局唯一 id 生成策略

1、数据库自增长序列或字段生成 id

    最常见的一种生成 id 方式。利用数据库本身来进行设置,在全数据库内保持唯一。

    【优点】

非常简单。利用现有数据库系统的功能实现,成本小,代码简单,性能可以接受。

ID 号单调递增。可以实现一些对 ID 有特殊要求的业务,比如对分页或者排序结果这类需求有帮助。

    【缺点】

    1. 强依赖 DB。不同数据库语法和实现不同,数据库迁移的时候、多数据库版本支持的时候、或分表分库的时候需要处理,会比较麻烦。当 DB 异常时整个系统不可用,属于致命问题。

      2. 单点故障。在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。

                 3. 数据一致性问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。

                    4. 难于扩展。在性能达不到要求的情况下,比较难于扩展。ID 发号性能瓶颈限制在单台 MySQL 的读写性能。

    【部分优化方案】

    针对主库单点, 如果有多个 Master 库,则每个 Master 库设置的起始数字不一样,步长一样,可以是 Master 的个数。比如:Master1 生成的是 1,4,7,10,Master2 生成的是 2,5,8,11 Master3 生成的是 3,6,9,12。这样就可以有效生成集群中的唯一 ID,也可以大大降低 ID 生成数据库操作的负载。

2、UUID

    常见的生成 id 方式,利用程序生成。

    UUID (Universally Unique Identifier) 的目的,是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。如此一来,每个人都可以建立不与其它人冲突的 UUID。在这样的情况下,就不需考虑数据库建立时的名称重复问题。

    UUID 的标准形式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前为止业界一共有 5 种方式生成 UUID,详情见 IETF 发布的 UUID 规范 A Universally Unique IDentifier (UUID) URN Namespace。

   在 Java 中我们可以直接使用下面的 API 生成 UUID:

UUID uuid  =  UUID.randomUUID(); String s = UUID.randomUUID().toString();

    【优点】

非常简单,本地生成,代码方便,API 调用方便。

性能非高。生成的 id 性能非常好,没有网络消耗,基本不会有性能问题。

全球唯一。在数据库迁移、系统数据合并、或者数据库变更的情况下,可以 从容应对。

    【缺点】

存储成本高。UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示,很多场景不适用。如果是海量数据库,就需要考虑存储量的问题。

信息不安全。基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

不适用作为主键,ID 作为主键时在特定的环境会存在一些问题,比如做 DB 主键的场景下,UUID 就非常不适用。UUID 往往是使用字符串存储,查询的效率比较低。

UUID 是无序的。不是单调递增的,而现阶段主流的数据库主键索引都是选用的 B + 树索引,对于无序长度过长的主键插入效率比较低。

传输数据量大。

不可读。

    【部分优化方案】

    为了解决 UUID 不可读, 可以使用 UUID to Int64 的方法 。

    为了解决 UUID 无序的问题, NHibernate 在其主键生成方式中提供了 Comb 算法(combined guid/timestamp)。保留 GUID 的 10 个字节,用另 6 个字节表示 GUID 生成的时间(DateTime)。

  3、Redis 生成 ID

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

    可以使用 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 来生成日切流水号。比如订单号 = 日期 + 当日自增长号。可以每天在 Redis 中生成一个 Key,使用 INCR 进行累加。

    【优点】

不依赖于数据库,灵活方便,且性能优于数据库。。

数字 ID 天然排序,对分页或者需要排序的结果很有帮助。

    【缺点】

如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度。。

需要编码和配置的工作量比较大。

Redis 单点故障,影响序列服务的可用性。

4、zookeeper 生成 ID

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

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

5、Twitter 的 snowflake 算法

    snowflake(雪花算法) 是 Twitter 开源的分布式 ID 生成算法,结果是一个 long 型的 ID。这种方案把 64-bit 分别划分成多段,分开来标示机器、时间等。如图:

image-20211108234247962

    其核心思想是:使用 41bit 作为毫秒数,10bit 作为机器的 ID(5 个 bit 是数据中心,5 个 bit 的机器 ID),12bit 作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是 0。具体实现的代码可以参看 github。

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

    【优点】

稳定性高,不依赖于数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。

灵活方便,可以根据自身业务特性分配 bit 位。

单机上 ID 单调自增,毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的。

    【缺点】

强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

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