分布式唯一ID生成

1、为什么需要分布式ID?

对于单体系统来说,主键ID可能会常用主键自动的方式进行设置,这种ID生成方法在单体项目是可行的,但是对于分布式系统,分库分表之后,就不适应了,比如订单表数据量太大了,分成了多个库,如果还采用数据库主键自增的方式,就会出现在不同库id一致的情况,虽然是不符合业务的

2、分布式唯一 ID 特性

在业务开发中,会存在大量的场景都需要唯一 ID 来进行标识。比如,用户需要唯一身份标识;商品需要唯一标识;消息需要唯一标识;事件需要唯一标识等等。尤其是在分布式场景下,业务会更加依赖唯一 ID。

分布式唯一 ID 的特性如下:

  • 全局唯一:必须保证生成的 ID 是全局性唯一的,这是分布式 ID 的基本要求;

  • 有序性:生成的 ID 需要按照某种规则有序,便于数据库的写入和排序操作;

  • 可用性:需要保证高并发下的可用性。除了对 ID 号码自身的要求,业务还对 ID 生成系统的可用性要求极高;

  • 自主性:分布式环境下不依赖中心认证即可自行生成 ID;

  • 安全性:不暴露系统和业务的信息。在一些业务场景下,会需要 ID 无规则或者不规则。

3. 常用分布式唯一 ID 生成方案

3.1. UUID

UUID(Universally Unique Identifier,即通用唯一标识码)算法的目的是生成某种形式的全局唯一 ID 来标识系统中的任一元素,尤其是在分布式环境下,UUID 可以不依赖中心认证即可自动生成全局唯一 ID。

UUID 的标准形式为 32 个十六进制数组成的字符串,且分割为五个部分,例如:467e8542-2275-4163-95d6-7adc205580a9。

基于使用场景的不同,会存在以下几个不同版本的 UUID 以供使用,如下所示:

  • 基于时间的 UUID:主要依赖当前的时间戳和机器 mac 地址。优势是能基本保证全球唯一性,缺点是由于使用了 mac 地址,会暴露 mac 地址和生成时间;

  • 分布式安全的 UUID:将基于时间的 UUID 算法中的时间戳前四位替换为 POSIX 的 UID 或 GID。优势是能保证全球唯一性,缺点是很少使用,常用库基本没有实现;

  • 基于随机数的 UUID:基于随机数或伪随机数生成。优势是实现简单,缺点是重复几率可计算;

  • 基于名字空间的 UUID(MD5 版):基于指定的名字空间/名字生成 MD5 散列值得到。优势是不同名字空间/名字下的 UUID 是唯一的,缺点是 MD5 碰撞问题,只用于向后兼容;

  • 基于名字空间的 UUID(SHA1 版):将基于名字空间的 UUID(MD5 版)中国的散列算法修改为 SHA1。优势是不同名字空间/名字下的 UUID 是唯一的,缺点是 SHA1 计算相对耗时。

UUID 的优势是性能非常高,由于是本地生成,没有网络消耗。而其也存在一些缺陷,包括不易于存储,UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示;信息不安全,基于时间的 UUID 可能会造成机器的 mac 地址泄露;ID 作为 DB 主键时在特定的场景下会存在一些问题。

优点

  • 性能非常高,本地生成的,不依赖于网络

缺点

  • 不易存储,16 字节128位,36位长度的字符串

  • 信息不安全,基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置

  • uuid的无序性可能会引起数据位置频繁变动,影响性能

3.2. 数据库自增 ID

在分布式环境也可以使用mysql的自增实现分布式ID的生成,如果分库分表了,当然不是简单的设置好auto_increment_increment和 auto_increment_offset 即可,在分布式系统中我们可以多部署几台机器,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器。

设置步长step为2,Server1的初始值为1(1,3,5,7,9,11…)、Server2的初始值为2(2,4,6,8,10…)。这是Flickr团队在2010年撰文介绍的一种主键生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap)

假设有N台机器,step就要设置为N,如图进行设置:

这种方案看起来是可行的,但是如果要扩容,步长step等要重新设置,假如只有一台机器,步长就是1,比如1,2,3,4,5,6,这时候如果要进行扩容,就要重新设置,机器2可以挑一个偶数的数字,这个数字在扩容时间内,数据库自增要达不到这个数的,然后步长就是2,机器1要重新设置step为2,然后还是以一个奇数开始进行自增。这个过程看起来不是很杂,但是,如果机器很多的话,那就要花很多时间去维护重新设置 这种实现的缺陷:

  • ID没有了单调递增的特性,只能趋势递增,有些业务场景可能不符合

  • 数据库压力还是比较大,每次获取ID都需要读取数据库,只能通过多台机器提高稳定性和性能

3.3、号段模式

这种模式也是现在生成分布式ID的一种方法,实现思路是会从数据库获取一个号段范围,比如[1,1000],生成1到1000的自增ID加载到内存中,建表结构如:

CREATE TABLE id_generator (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '当前最大id',
  step int(20) NOT NULL COMMENT '号段的布长',
  biz_type int(20) NOT NULL COMMENT '业务类型',
  version int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
) 
  • biz_type :不同业务类型

  • max_id :当前最大的id

  • step :代表号段的步长

  • version :版本号,就像MVCC一样,可以理解为乐观锁

等ID都用了,再去数据库获取,然后更改最大值

update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
  • 优点:有比较成熟的方案,像百度Uidgenerator,美团Leaf

  • 缺点:依赖于数据库实现

3.4、 Redis实现

Redis分布式ID实现主要是通过提供像INCR 和 INCRBY 这样的自增原子命令,由于Redis单线程的特点,可以保证ID的唯一性和有序性

这种实现方式,如果并发请求量上来后,就需要集群,不过集群后,又要和传统数据库一样,设置分段和步长

优缺点:

  • 优点:Redis性能相对比较好,又可以保证唯一性和有序性

  • 缺点:需要依赖Redis来实现,系统需要引进Redis组件

3.5. Snowflake 算法

snowflake(雪花算法)是一个开源的分布式 ID 生成算法,结果是一个 long 型的 ID。snowflake 算法将 64bit 划分为多段,分开来标识机器、时间等信息,具体组成结构如下图所示:

snowflake 算法的核心思想是使用 41bit 作为毫秒数,10bit 作为机器的 ID(比如其中 5 个 bit 可作为数据中心,5 个 bit 作为机器 ID),12bit 作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是 0。

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

snowflake 算法的优势是稳定性高,不依赖于数据库等第三方系统;使用灵活方便,可以根据业务需求的特性来调整算法中的 bit 位;单机上 ID 单调自增,毫秒数在高位,自增序列在低位,整个 ID 是趋势递增的。而其也存在一定的缺陷,包括强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务处于不可用状态;ID 可能不是全局递增,虽然 ID 在单机上是递增的,但是由于涉及到分布式环境下的每个机器节点上的时钟,可能会出现不是全局递增的场景。

优点:雪花算法生成的ID是趋势递增,不依赖数据库等第三方系统,生成ID的效率非常高,稳定性好,可以根据自身业务特性分配bit位,比较灵活

缺点:雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。

3.6、 百度Uidgenerator

UidGenerator 方案是基于 snowflake 算法的唯一 ID 生成器。其对雪花算法的 bit 位的分配做了微调,如下图所示:

UidGenerator 方案包含以下两种实现方式:

1)DefaultUidGenerator 实现方式

DefaultUidGenerator 方式的实现要点如下所示:

  • delta seconds:在上图中用 28bit 部分表示,指当前时间与 epoch 时间的时间差,单位为秒。epoch 时间指集成 DefaultUidGenerator 生成分布式 ID 服务第一次上线的时间,可配置。

  • worker id:在上图中用 22bit 部分表示,在使用 DefaultUidGenerator 方式生成分布式 ID 的实例启动的时候,往 db 中写入一行数据得到的自增 id 值。由于 worker id 默认 22 位,允许集成 DefaultUidGenerator 生成分布式 id 的所有实例的重启次数不超过 4194303 次,否则会抛出异常

  • sequence:在上图中用 13bit 部分表示,通过 synchronized 保证线程安全;如果时间有任何的回拨,直接抛出异常;如果当前时间和上一次是同一秒时间,sequence 自增,如果同一秒内自增至超过 2^13-1,自旋等待下一秒;如果是新的一秒,sequence 从 0 开始。

DefaultUidGenerator 方式在出现任何刻度的时钟回拨时都会直接抛异常给到业务层,实现比较简单粗暴。故使用 DefaultUidGenerator 方式生成分布式 ID,需要根据业务情况和特点,调整各个字段占用的位数。

2)CachedUidGenerator 实现方式

CachedUidGenerator 的核心是利用 RingBuffer,本质上是一个数组,数组中每个项被称为 slot。CachedUidGenerator 设计了两个 RingBuffer,一个保存唯一 ID,一个保存 flag。其实现要点如下所示:

  • 自增列:UidGenerator 的 workerId 在实例每次重启时初始化,且就是数据库的自增 ID,从而完美的实现每个实例获取到的 workerId 不会有任何冲突。

  • RingBuffer:UidGenerator 不再在每次取 ID 时都实时计算分布式 ID,而是利用 RingBuffer 数据结构预先生成若干个分布式 ID 并保存。

  • 时间递增:UidGenerator 的时间类型是 AtomicLong,且通过 incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题。

3.7、美团 Leaf-snowflake 方案

Leaf-snowflake 方案沿用 snowflake 方案的 bit 位设计,即”1+41+10+12“的方式组装 ID 号(正数位(占 1 比特)+ 时间戳(占 41 比特)+ 机器 ID(占 5 比特)+ 机房 ID(占 5 比特)+ 自增值(占 12 比特)),如下图所示:

对于 workerID 的分配,当服务集群较小时,通过配置即可;当服务集群较大时,基于 zookeeper 持久顺序节点的特性引入 zookeeper 组件配置 workerID。部署架构如下图所示:

Leaf-snowflake 方案在处理时钟回拨问题的策略如下所示:

1)服务启动时

  • 在服务启动时,首先检查自己是否写过 zookeeper leaf_forever 节点;

  • 如果写过,则用自身系统时间与 leaf_forever/${self}节点记录时间做比较,若小于则认为机器时间发生了大步长回拨,服务启动失败并告警;

  • 如果没有写过,直接创建持久节点 leaf_forever/${self},并写入自身系统时间;

  • 然后取 leaf_temporary 下的所有临时节点(所有运行中的 Leaf-snowflake 节点)的服务 IP:Port,然后通过 RPC 请求得到所有节点的系统时间,计算 sum(time)/nodeSize;

  • 如果若 abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点 leaf_temporary/${self} 维持租约;否则认为本机系统时间发生大步长偏移,启动失败并报警;

  • 每隔一段时间(3s)上报自身系统时间写入 leaf_forever/${self}。

2)服务运行时

  • 会检查时钟回拨时间是否小于 5ms,若时钟回拨时间小于等于 5ms,等待时钟回拨时间后,重新产生新的 ID;若时钟回拨时间大于 5ms,直接抛异常给到业务侧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java分布式唯一ID生成方案有很多种,其中一种比较常用的方案是基于Snowflake算法的ID生成方案。 Snowflake算法是一种基于时间序列生成唯一ID的算法,它可以在分布式系统中生成唯一的、有序的、趋势递增的ID。Snowflake算法生成ID是一个64位的整数,它的结构如下: ``` 0 - 41位时间戳 - 10位机器标识 - 12位序列号 ``` 其中,时间戳占用了41位,可以表示2^41-1个数字,大约可以支持生成69年的ID;机器标识占用了10位,可以表示1023个不同的机器;序列号占用了12位,可以表示4095个不同的序列号。 Snowflake算法的实现比较简单,可以使用Java的AtomicLong类来实现序列号的自增。具体的实现可以参考下面的代码: ```java public class SnowflakeIdGenerator { private static final long START_TIMESTAMP = 1577808000000L; // 2020-01-01 00:00:00 private static final long MACHINE_ID_BITS = 10L; private static final long SEQUENCE_BITS = 12L; private static final long MACHINE_ID_OFFSET = SEQUENCE_BITS; private static final long TIMESTAMP_OFFSET = MACHINE_ID_BITS + SEQUENCE_BITS; private static final long MAX_MACHINE_ID = (1L << MACHINE_ID_BITS) - 1L; private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1L; private static long machineId = 0L; private static long sequence = 0L; private static long lastTimestamp = -1L; static { String machineName = ""; try { machineName = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { e.printStackTrace(); } machineId = Math.abs(machineName.hashCode()) % MAX_MACHINE_ID; } public synchronized static long nextId() { long currentTimestamp = System.currentTimeMillis(); if (currentTimestamp < lastTimestamp) { throw new IllegalStateException("Clock moved backwards. Refusing to generate id"); } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1L) & MAX_SEQUENCE; if (sequence == 0L) { currentTimestamp = waitUntilNextMillis(currentTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_OFFSET) | (machineId << MACHINE_ID_OFFSET) | sequence; } private static long waitUntilNextMillis(long currentTimestamp) { while (currentTimestamp == lastTimestamp) { currentTimestamp = System.currentTimeMillis(); } return currentTimestamp; } } ``` 使用这个类可以生成全局唯一ID,可以在分布式系统中使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code.song

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值