UID Generator
为什么要使用
由于 Event Soucing 是记录事件的,那么 Object Id 肯定就不能是用数据库生成的了,基本上所有的 Event Soucing 相关的框架都是将事件直接序列化,然后对应到 Object,所以这种情况下,就需要自己产生 ID,而自己生成 ID 的话,就有很多限制,比如需要根据时间递增,尽量比较短,在分布式的情况下 ID 保证不能重复等等,本文会比较几种方案,然后选择一种比较好的来实现。
方案选择
数据库
这种方案其实就是基于数据库的自增 ID,各个分布式系统通过一个数据库去分配 ID,由于依赖了数据库,性能肯定是个问题,如果部署多点数据库,不但实现麻烦,而且性能还是取决于数据库数量,所以在分布式系统当中,并发量大的系统一般不会采取该方案。
UUID
UUID是通用唯一识别码 (Universally Unique Identifier),是由一组 32 位数的 16 进制数字所构成,也就是 128 bit。在规范字符串格式中,UUID 的十六个八位字节被表示为 32个十六进制(基数16)数字,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。例如:
1 | 123e4567-e89b-12d3-a456-426655440000 |
N那个位置,只会是8,9,a,b, M那个位置,代表版本号,由于UUID的标准实现有5个版本,所以只会是1,2,3,4,5。不同的版本基于的算法不一样,而在 Java 中最常用的 UUID.randomUUID()
是基于版本 4 的,基于随机数,也会有重复的概率,只是概率特别低,低到几乎可以忽略而已。
由于这种算法生成的 ID 是字符串,而且长度有特别的长,非常不利于建立索引等操作,所以通常不会用来作为主键。
Snowflake
为了满足在分布式系统中可以生成全局唯一且趋势递增的 ID,Twitter 推出了一种算法,该算法由 64 bit 组成。
- 第一位永远是0,实际上这是为了让生成的 ID 都为正数,以保证趋势递增;
- 后面 41 位用来记录时间,理论上可以记录 2^41 毫秒,2^41/(24 * 3600 * 365 * 1000) = 69.7 年,所以这里的理论最大使用时间就是 70 年左右;
- 在后面 10 位用来记录机器 ID ,更准确的应该说是实例 ID,对应的可以是某个 Container 或者某个进程,最多支持 1024 个;
- 最后12位用来记录序列号,来保证每个实例每毫秒生成的 ID 唯一;
该算法的优点:
- 不依赖数据库,高性能;
- 生成的 ID 趋势递增;
- 64 bit 的数字作为 ID 相比 UUID 短的多,方便建立数据库索引。
该算法的缺点:
- 依赖系统时钟,如果系统时钟发生回拨,那么有可能造成 ID 冲突或乱序。
基于 Java 的实现
基于上面的分析,这里我们选择使用 Snowflake 来实现,Twitter 官方提供了一个 Scala 版本的实现,在这里我们实现一个 Java 版本,具体代码如下:
1 | 4j |
仔细阅读以下这段代码,你会发现有个地方和我们描述的不太一样
1 | if (currStamp == lastStamp) { |
这里再毫秒刷新的时候,我们并没有去把序列号置为 0,而是随机从 0-9 取了一个数,这么做的原因是在并发量不是特别高的时候,如果都从 0 开始的话,会导致生成的 ID 都是偶数,那么在做一些分表操作的时候,会导致严重的分配不均匀,所以这里我们随机从 0-9 开始让产生的 ID 尽可能的分配均匀。但是这么做是会下降性能的,每毫秒的 ID 生成数量会下降一些,但是这并没有下降数量级,完全是可以接受的。
基于 Spring Cloud 分配 Worker Id
上面介绍了如何使用 Snowflake 来生成 ID,那么结合 Spring Cloud ,我们需要给每个节点分配一个 Worker ID,但是由于 Spring Cloud 的特点,它是希望每个节点无状态化的,这就给我们分配 Worker ID 带来了一定的难度,如果我们需要区分每个几点,就不得不将节点信息存储到某个中央,然后再分配,为了便于之后的水平扩展,这里我们基于内部代码实现,大概的原理是在服务启动的时候,记录下节点 IP 和 MAC ,作为 Service Node Key 存储到数据库,这个 Key 在数据库中唯一,通过这个唯一的 Key 给不同的节点分配 ID。下面我们尝试使用 JPA 来实现这一过程。
Spring Cloud 在 2.1.0 之后提供了 getInstanceId()
方法,但是可以为空,所以需要看各个具体实现,我看了 K8S 和 consul 都提供了该方法的实现
1 |
|
建立相应的 Entity、Repository、Service,从代码中可以看到 Worker ID 的实现,获取 IP 和 MAC 作为唯一 Key 存入数据库,获取到自增 ID,然后对 Snowflake 的最大 Worker ID 取余,这样便得到了一个可用的 Worker ID。
然而这么做会不会有问题?由于Worker ID 在 0-1023 之间反复,如果某些节点反复重启,超过 1024 次并且一些节点一直没有重启,就会出现 Worker ID 重复的情况。由于我们的业务目前节点的更新一般都是逐个依次重启,所以这里暂时不去处理这个问题,未来如果需要多个节点进行 AB 测试,这个时候可能就会出现某些节点频繁更新,而某些节点不变化的情况,届时可能就要重新考虑分配 ID 的方案了。
下面继续完成上面的方案,我们已经写好了相关的 Service ,剩下的就是在服务启动的时候向数据库写入信息了。
1 |
|
这里我们建立一个 UIDGenerator
作为服务的 ID 生成器,在启动的时候,通过 WorkerIdService
获取到一个 Worker ID,并构建 Snowfalke
对象,至此我们的 ID 生成器就基本完成了。