ID生成器第一讲:原理和常见几种生成器

一、介绍

ID 是标识符(identifier)的前缀,它代表一个可以唯一识别一个对象或者物体的名称。在软件系统中,ID 用于对一组信息进行标识,它是信息系统里最底层、最基础的概念,从系统诞生到消亡,都与 ID 息息相关。

在分布式系统中,ID生成器是至关重要的,因为分布式系统中,一个数据表可能存储在多个物理机上,这样主键Id如何生成,怎么保证其有序性,生成可靠性,生成稳定性等都是一个问题。

作为一个Java后端架构师,我们需要知道 ID生成器的原理和常见的ID生成器都有哪些,以及如何自己实现一个简易的Id生成器,加深对分布式系统的理解。

二、属性

用5 个属性来分析

  • 自身属性: 类型、长度
  • 领域属性: 唯一性、稀疏度、递增性

三、各种Id生成

1、db自增ID表


介绍: 自增ID表其实就是给db中写一张id表,利用数据库主键id自增思想来生成id.

java 复制代码
create table `id_gen` 
(
    `id` bigint(20) not null auto_increment,
    primary key(`id`)
) engine=InnoDB DEFAULT CHATSET = utf8mb4;

优点 : 实现简单,成本不高,自增连续性好,Id严格从1增长
缺点:

css 复制代码
- 单表<font style="color:#DF2A3F;">过热</font>,<font style="color:#DF2A3F;">qps存在瓶颈</font>。
- <font style="color:#DF2A3F;">安全问题</font>(根据订单ID递增规则推送出每天的数据量等等)
- 每获取一次Id都需要访问一次数据库,单个db的<font style="color:#DF2A3F;">连接数</font>是瓶颈.

2、db多主模式


介绍: 多个表同时提供ID服务,一个表从1开始步长为2,一个表从2开始步长2。

生产id: 1, 3, 5. 2, 4, 6.

如果需要新增一个db实例,就需要重新调整.

比较繁琐.

set @@auto_increment_offset = 2; --- 起始值

set @@auto_increment_increment= 2; --- 步长

优点 : 分散db压力
缺点: 扩展性和复杂度提高, 其实并没有实现分布式

3、号段模式

介绍:

数据库每次递增一条肯定是不行的,所以能否一次请求,生成一段Id, 放在内存中,下次直接访问内存即可, 这样减轻db请求次数. 这就是我们所说的基于数据库号段生成分布式Id。

具体实现:

  • Idserver 多个rpc生成: 瓶颈在于id server 数量
  • 客户端批量生成: 瓶颈在于DB,多个实例同时请求同一个业务的更新权限时,其实就是会抢行锁.
  • 如果是认为单表过热的话,也就是多个业务,其实可以横向扩充shard即可. biz_id % shard 去操作.

idserver我们就不用多说了,其实就是单个单个server组成集群,每个server提供id生成服务.

我们下面来看一下客户端批量生成单表.

java 复制代码
create table `id_generator` (
    `id` bigint(20) not null,
    `max_id` bigint(20) not null comment '最大id',
    `step` int(10) not null comment '号段长度',
    `version` int(20) not null comment '版本',
    `biz_type` int(20) not null comment '业务类型',
    primary key(`id`)
) engine = InnoDB default charset=utf8mb4;

select + update 重试

java 复制代码
update ml_id_gen set version=version + 1, max_id =newMaxId where biz_id=:bizId and version = version 
如果updateCount == 0, 重复select, update直到修改成功


注意: newMaxId = oldMaxId + step 

// 那就得先获取版本, 再更新.
select version from ml_id_gen where biz_id = :bizId


在获取层client, 内部就可以封装一个loadingCache<bizId, Tuple<Long, Long>>   
        Tuple中是 curId, curRegionMaxId

case: 
1. bizId 无, 去找db 生成
2. bizId 有  
        2.1 curId = curRegionMaxId
        找server分配,  更新cache
        重新拿到curId, curId++更新cache.
        return 重新拿到curId;
        2.2 curId < curRegionMaxId
        long oldId = curId++;
        更新cache(curId, curRegionMaxId)
        return oldId;

优点: ID 趋势增长、存储消耗空间少

缺点 : 单表存在热点问题,Id 没有意义,安全问题(步长很容易被猜出来)

4、NoSQL自增

介绍:

采用incr自增获取.

**优点: **性能非常好,并且Id有序增长。

缺点:

markdown 复制代码
- **存在单点.  当然有集群.**
- **RDB**:   每隔一段时间快照,但是如果连续incr,redis挂了,会造成id重复.
- **AOF**:   不会重复,但是incr命令过多,导致重启恢复数据时间过长.

5、雪花算法及其多个实现

**介绍: **

雪花算法: snowflow是twitter开源的分布式Id生成算法.

long 我们都知道8个字节==64bit.

diff 复制代码
- 第一位标识:  0: 正数  1: 负数
- 41位:  时间戳部分ms,  一般是当前时间 - 固定的开始时间,这样产生的ID可以从更小值开始.   而且41位可以用 (1L << 41 / (1000 * 3600 * 24 * 365)) = 69年.
- 10位: 工作机器id,5位为机房信息,5位为机房中的机器号 workerId, 这样可以部署1024个节点.
- 12位: 序列号部分, 支持统一ms内同一个节点可以生成4096个ID.

优点: 生成速度快、生成的ID有序递增、比较灵活(可以改造)

缺点: 需要解决重复ID问题 (比较依赖服务器时间,在机器时间不对的情况下, 可能会造成重复ID)

后面百度uid-generator, 美团leaf,滴滴Tinyid等都是修改 这个几个的位数和 具体workerId怎样生成的。

Uid-Generator(百度):

uid-generator 基于Java、snawflake实现的唯一Id生成器。

看起来已经不在维护了.

Leaf(美团):

Leaf 最早期需求是各个业务线的订单ID生成需求。\ 看起来以前他们是db自增,redis自增来生成ID,后面他们统一了。\ 这其实得深入了解美团的技术组成,如果内部有两个相同的业务,就会整合成一个,这一点非常好.

世界上没有两片完全相同的树叶,人也是一样.

Leaf 提供了 号段模式 和 snowflow(雪花算法) 这两种模式来生成分布式ID。

支持双号段,解决了雪花ID 系统时钟回拨问题。但是时钟问题的解决依赖于Zk。

雪花的号段位分布. Leaf------美团点评分布式ID生成系统

主要是workerId生成不同,leaf中workId 是基于zk的顺序id来生成的,每个应用在使用Leaf-snowFlake时,在启动时都会在zk中生成一个顺序id,相当于一个机器对应一个顺序节点,也就是一个workId。

Tinyid(滴滴):

Tinyid(opens new window)是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。

相关文档: Tinyid原理介绍

到此一个简单的id生成系统就完成了,那么是否还存在问题呢?回想一下我们最开始的id生成系统要求,高性能、高可用、简单易用,在上面这套架构里,至少还存在以下问题:

  • 当id用完时需要访问db加载新的号段,db更新也可能存在version冲突,此时id生成耗时明显增加
  • db是一个单点,虽然db可以建设主从等高可用架构,但始终是一个单点
  • 使用http方式获取一个id,存在网络开销,性能和可用性都不太好

所以后面加了下面这几个来优化.

  • 双号段缓存:
    • 为了避免在获取新号段的情况下,获取唯一ID的速度比较慢。Tinyid中的号段在加载到一定的程度(比如一半),就去异步加载下一个号段,保证内存中永远始终有可用的号段。
  • 增加多db支持 其实就是生成奇数,偶数这种.
    • 支持多个DB,并且,每个DB都能生成唯一ID,提高可用性。
  • 增加tinyid-client。
    • 纯纯本地操作,无http请求损耗。

优缺点: 上面也说了很多,但是要注意数据库号段的缺点。

6、UUID

介绍:

UUID 是 Universally unique identifier (通用唯一标识符) 的 缩写。UUID 包含32个16进入数字 (8-4-4-4-12) 。

JDK 中就用现成的。

java 复制代码
final UUID uuid = UUID.randomUUID();
System.out.println(uuid);
System.out.println(uuid.version());


4c994944-82e1-4be4-a543-ab182d87b11d
4

RFC 4122(opens new window)中关于 UUID 的示例是这样的:

其实需要关注版本即可,不同的版本对应的UUID 的生成规则是不同的。

5 种不同的 Version(版本)值分别对应的含义(参考维基百科对于 UUID 的介绍(opens new window)):

  • 版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
  • 版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
  • 版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成;
  • 版本 4 : UUID 使用随机性(opens new window)伪随机性(opens new window)生成。

下面是 Version 1 版本下生成的 UUID 的示例:

JDK 中通过 UUID 的 randomUUID() 方法生成的 UUID 的版本默认为 4。

我们看UUID的生成包含: MAC地址、时间戳、命名空间、随机或伪随机数、时序等元素,当然极少概率会出现重复.

优点: 生成速度比较快、简单

缺点: 存储消耗空间大(36字符,128位)、 不安全(基于MAC地址生成会造成MAC地址泄漏)、无序、没有具体的业务含义、需要解决重复ID问题 (当机器时间不对的时候, linux 的ntp时间会有问题, 可能会导致重复Id)

说明:

  • 其中1.2.3.4 数据自增思想.
  • 5属于snowFlake思想,各个bit位数不一样,workerId生成不一样.

四、使用

其实在大厂里都有自己的id生成器,一般都是雪花算法的变种,当然也提供类似号段模式,id-server严格递增等等。

还是需要根据业务诉求、背景去找到符合自己背景的id生成器

附录:

www.infoq.cn/article/iuv...

javaguide.cn/distributed...

相关推荐
q***73551 小时前
SpringBoot中使用TraceId进行日志追踪
spring boot·后端·状态模式
Penge6661 小时前
Elasticsearch 跳表(Skip List):有序结果合并的 “性能电梯”
后端
Penge6661 小时前
Elasticsearch BKD 树与 PointRangeQuery:为何数值查询会有性能瓶颈
后端
木木一直在哭泣1 小时前
【收藏级】Java Stream.reduce 全面解析:从零到通透(原理图 + 实战 + 最佳实践)
后端
Penge6661 小时前
Elasticsearch Filter 缓存:Bitset 如何让查询速度飙升
后端
用户84913717547161 小时前
ThreadLocal 源码深度解析:JDK 设计者的“妥协”与“智慧”
java·后端
木木一直在哭泣1 小时前
Java Stream.filter 全面解析:定义、原理与最常见使用场景
后端
用户0304805912631 小时前
# 【Maven避坑】源码去哪了?一文看懂 Maven 工程与打包后的目录映射关系
java·后端
绫语宁2 小时前
以防你不知道LLM小技巧!为什么 LLM 不适合多任务推理?
人工智能·后端