关于java:Java面试指北为什么需要分布式ID大厂的分布式-ID-生成方案是什么样的-JavaGuide

29次阅读

共计 7482 个字符,预计需要花费 19 分钟才能阅读完成。

今日举荐:Github 标星 100k!2021 最新 Java 学习线路图是怎么的?

下午好,我是 Guide 哥!

明天分享一道敌人去京东面试实在遇到的面试题:“为什么要分布式 ID?你我的项目中是怎么做的?”。

这篇文章我会说说本人的认识,具体介绍一下分布式 ID 相干的内容包含分布式 ID 的根本要求以及分布式 ID 常见的解决方案。

这篇文章全程都是大白话的模式,心愿可能为你带来帮忙!

原创不易,若有帮忙,点赞 / 分享就是对我最大的激励!

集体能力无限。如果文章有任何须要补充 / 欠缺 / 批改的中央,欢送在评论区指出,共同进步!

分布式 ID

何为 ID?

日常开发中,咱们须要对系统中的各种数据应用 ID 惟一示意,比方用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。

咱们现实生活中也有各种 ID,比方身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应

简略来说,ID 就是数据的惟一标识

何为分布式 ID?

分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。

我简略举一个分库分表的例子。

我司的一个我的项目,应用的是单机 MySQL。然而,没想到的是,我的项目上线一个月之后,随着应用人数越来越多,整个零碎的数据量将越来越大。

单机 MySQL 曾经没方法撑持了,须要进行分库分表(举荐 Sharding-JDBC)。

在分库之后,数据遍布在不同服务器上的数据库,数据库的自增主键曾经没方法满足生成的主键惟一了。咱们如何为不同的数据节点生成全局惟一主键呢?

这个时候就须要生成 分布式 ID了。

分布式 ID 须要满足哪些要求?

分布式 ID 作为分布式系统中必不可少的一环,很多中央都要用到分布式 ID。

一个最根本的分布式 ID 须要满足上面这些要求:

  • 全局惟一:ID 的全局唯一性必定是首先要满足的!
  • 高性能:分布式 ID 的生成速度要快,对本地资源耗费要小。
  • 高可用:生成分布式 ID 的服务要保障可用性有限靠近于 100%。
  • 不便易用:拿来即用,使用方便,疾速接入!

除了这些之外,一个比拟好的分布式 ID 还应保障:

  • 平安:ID 中不蕴含敏感信息。
  • 有序递增:如果要把 ID 寄存在数据库的话,ID 的有序性能够晋升数据库写入速度。并且,很多时候,咱们还很有可能会间接通过 ID 来进行排序。
  • 有具体的业务含意:生成的 ID 如果能有具体的业务含意,能够让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
  • 独立部署:也就是分布式系统独自有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务能够和业务相干的服务解耦。不过,这样同样带来了网络调用耗费减少的问题。总的来说,如果须要用到分布式 ID 的场景比拟多的话,独立部署的发号器服务还是很有必要的。

分布式 ID 常见解决方案

数据库

数据库主键自增

这种形式就比较简单直白了,就是通过关系型数据库的自增主键产生来惟一的 ID。

以 MySQL 举例,咱们通过上面的形式即可。

1. 创立一个数据库表。

CREATE TABLE `sequence_id` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `stub` char(10) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stub 字段无意义,只是为了占位,便于咱们插入或者批改数据。并且,给 stub 字段创立了惟一索引,保障其唯一性。

2. 通过 replace into 来插入数据。

BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;

插入数据这里,咱们没有应用 insert into 而是应用 replace into 来插入数据,具体步骤是这样的:

1)第一步:尝试把数据插入到表中。

2)第二步:如果主键或惟一索引字段呈现反复数据谬误而插入失败时,先从表中删除含有反复关键字值的抵触行,而后再次尝试把数据插入到表中。

这种形式的优缺点也比拟显著:

  • 长处:实现起来比较简单、ID 有序递增、存储耗费空间小
  • 毛病:反对的并发量不大、存在数据库单点问题(能够应用数据库集群解决,不过减少了复杂度)、ID 没有具体业务含意、平安问题(比方依据订单 ID 的递增法则就能推算出每天的订单量,商业秘密啊!)、每次获取 ID 都要拜访一次数据库(减少了对数据库的压力,获取速度也慢)

数据库号段模式

数据库主键自增这种模式,每次获取 ID 都要拜访一次数据库,ID 需要比拟大的时候,必定是不行的。

如果咱们能够批量获取,而后存在在内存外面,须要用到的时候,间接从内存外面拿就难受了!这也就是咱们说的 基于数据库的号段模式来生成分布式 ID。

数据库的号段模式也是目前比拟支流的一种分布式 ID 生成形式。像滴滴开源的 Tinyid 就是基于这种形式来做的。不过,TinyId 应用了双号段缓存、减少多 db 反对等形式来进一步优化。

以 MySQL 举例,咱们通过上面的形式即可。

1. 创立一个数据库表。

CREATE TABLE `sequence_id_generator` (`id` int(10) NOT NULL,
  `current_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;

current_max_id 字段和 step 字段次要用于获取批量 ID,获取的批量 id 为:current_max_id ~ current_max_id+step

version 字段次要用于解决并发问题(乐观锁),biz_type 次要用于示意业余类型。

2. 先插入一行数据。

INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES
    (1, 0, 100, 0, 101);

3. 通过 SELECT 获取指定业务下的批量惟一 ID

SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101

后果:

id    current_max_id    step    version    biz_type
1    0    100    1    101

4. 不够用的话,更新之后从新 SELECT 即可。

UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0  AND `biz_type` = 101
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101

后果:

id    current_max_id    step    version    biz_type
1    100    100    1    101

相比于数据库主键自增的形式,数据库的号段模式对于数据库的拜访次数更少,数据库压力更小。

另外,为了防止单点问题,你能够从应用主从模式来进步可用性。

数据库号段模式的优缺点:

  • 长处:ID 有序递增、存储耗费空间小
  • 毛病:存在数据库单点问题(能够应用数据库集群解决,不过减少了复杂度)、ID 没有具体业务含意、平安问题(比方依据订单 ID 的递增法则就能推算出每天的订单量,商业秘密啊!)

NoSQL

个别状况下,NoSQL 计划应用 Redis 多一些。咱们通过 Redis 的 incr 命令即可实现对 id 原子程序递增。

127.0.0.1:6379> set sequence_id_biz_type 1
OK
127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2
127.0.0.1:6379> get sequence_id_biz_type
"2"

为了进步可用性和并发,咱们能够应用 Redis Cluser。Redis Cluser 是 Redis 官网提供的 Redis 集群解决方案(3.0+ 版本)。

除了 Redis Cluser 之外,你也能够应用开源的 Redis 集群计划 Codis(大规模集群比方上百个节点的时候比拟举荐)。

除了高可用和并发之外,咱们晓得 Redis 基于内存,咱们须要长久化数据,防止重启机器或者机器故障后数据失落。Redis 反对两种不同的长久化形式:快照(snapshotting,RDB)只追加文件(append-only file, AOF)。并且,Redis 4.0 开始反对 RDB 和 AOF 的混合长久化(默认敞开,能够通过配置项 aof-use-rdb-preamble 开启)。

对于 Redis 长久化,我这里就不过多介绍。不理解这部分内容的小伙伴,能够看看 JavaGuide 对于 Redis 知识点的总结。

Redis 计划的优缺点:

  • 长处:性能不错并且生成的 ID 是有序递增的
  • 毛病:和数据库主键自增计划的毛病相似

除了 Redis 之外,MongoDB ObjectId 常常也会被拿来当做分布式 ID 的解决方案。

MongoDB ObjectId 一共须要 12 个字节存储:

  • 0~3:工夫戳
  • 3~6:代表机器 ID
  • 7~8:机器过程 ID
  • 9~11:自增值

MongoDB 计划的优缺点:

  • 长处:性能不错并且生成的 ID 是有序递增的
  • 毛病:须要解决反复 ID 问题(当机器工夫不对的状况下,可能导致会产生反复 ID)、有安全性问题(ID 生成有规律性)

算法

UUID

UUID 是 Universally Unique Identifier(通用惟一标识符)的缩写。UUID 蕴含 32 个 16 进制数字(8-4-4-4-12)。

JDK 就提供了现成的生成 UUID 的办法,一行代码就行了。

// 输入示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa
UUID.randomUUID()

RFC 4122 中对于 UUID 的示例是这样的:

咱们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规定是不同的。

5 种不同的 Version(版本)值别离对应的含意(参考维基百科对于 UUID 的介绍):

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

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

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

UUID uuid = UUID.randomUUID();
int version = uuid.version();// 4

另外,Variant(变体)也有 4 种不同的值,这种值别离对应不同的含意。这里就不介绍了,貌似平时也不怎么须要关注。

须要用到的时候,去看看维基百科对于 UUID 的 Variant(变体) 相干的介绍即可。

从下面的介绍中能够看出,UUID 能够保障唯一性,因为其生成规定包含 MAC 地址、工夫戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规定生成的 UUID 是必定不会反复的。

尽管,UUID 能够做到全局唯一性,然而,咱们个别很少会应用它。

比方应用 UUID 作为 MySQL 数据库主键的时候就十分不适合:

  • 数据库主键要尽量越短越好,而 UUID 的耗费的存储空间比拟大(32 个字符串,128 位)。
  • UUID 是无程序的,InnoDB 引擎下,数据库主键的无序性会重大影响数据库性能。

最初,咱们再简略剖析一下 UUID 的优缺点(面试的时候可能会被问到的哦!):

  • 长处:生成速度比拟快、简略易用
  • 毛病:存储耗费空间大(32 个字符串,128 位)、不平安(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含意、须要解决反复 ID 问题(当机器工夫不对的状况下,可能导致会产生反复 ID)

Snowflake(雪花算法)

Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几局部,每一部分存储的数据都有特定的含意:

  • 第 0 位:符号位(标识正负),始终为 0,没有用,不必管。
  • 第 1~41 位:一共 41 位,用来示意工夫戳,单位是毫秒,能够撑持 2 ^41 毫秒(约 69 年)
  • 第 42~52 位:一共 10 位,一般来说,前 5 位示意机房 ID,后 5 位示意机器 ID(理论我的项目中能够依据理论状况调整)。这样就能够辨别不同集群 / 机房的节点。
  • 第 53~64 位:一共 12 位,用来示意序列号。序列号为自增值,代表单台机器每毫秒可能产生的最大 ID 数(2^12 = 4096), 也就是说单台机器每毫秒最多能够生成 4096 个 惟一 ID。

如果你想要应用 Snowflake 算法的话,个别不须要你本人再造轮子。有很多基于 Snowflake 算法的开源实现比方美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。

另外,在理论我的项目中,咱们个别也会对 Snowflake 算法进行革新,最常见的就是在 Snowflake 算法生成的 ID 中退出业务类型信息。

咱们再来看看 Snowflake 算法的优缺点:

  • 长处:生成速度比拟快、生成的 ID 有序递增、比拟灵便(能够对 Snowflake 算法进行简略的革新比方退出业务 ID)
  • 毛病:须要解决反复 ID 问题(依赖工夫,当机器工夫不对的状况下,可能导致会产生反复 ID)。

开源框架

UidGenerator(百度)

UidGenerator 是百度开源的一款基于 Snowflake(雪花算法)的惟一 ID 生成器。

不过,UidGenerator 对 Snowflake(雪花算法)进行了改良,生成的惟一 ID 组成如下。

能够看出,和原始 Snowflake(雪花算法)生成的惟一 ID 的组成不太一样。并且,下面这些参数咱们都能够自定义。

UidGenerator 官网文档中的介绍如下:

自 18 年后,UidGenerator 就根本没有再保护了,我这里也不过多介绍。想要进一步理解的敌人,能够看看 UidGenerator 的官网介绍。

Leaf(美团)

Leaf 是美团开源的一个分布式 ID 解决方案。这个我的项目的名字 Leaf(树叶)起源于德国哲学家、数学家莱布尼茨的一句话:“There are no two identical leaves in the world”(世界上没有两片雷同的树叶)。这名字起得真心挺不错的,有点文艺青年那味了!

Leaf 提供了 号段模式Snowflake(雪花算法) 这两种模式来生成分布式 ID。并且,它反对双号段,还解决了雪花 ID 零碎时钟回拨问题。不过,时钟问题的解决须要弱依赖于 Zookeeper。

Leaf 的诞生次要是为了解决美团各个业务线生成分布式 ID 的办法多种多样以及不牢靠的问题。

Leaf 对原有的号段模式进行改良,比方它这里减少了双号段防止获取 DB 在获取号段的时候阻塞申请获取 ID 的线程。简略来说,就是我一个号段还没用完之前,我本人就被动提前去获取下一个号段(图片来自于美团官网文章:《Leaf——美团点评分布式 ID 生成零碎》)。

依据我的项目 README 介绍,在 4C8G VM 根底上,通过公司 RPC 形式调用,QPS 压测后果近 5w/s,TP999 1ms。

Tinyid(滴滴)

Tinyid 是滴滴开源的一款基于数据库号段模式的惟一 ID 生成器。

数据库号段模式的原理咱们在下面曾经介绍过了。Tinyid 有哪些亮点呢?

为了搞清楚这个问题,咱们先来看看基于数据库号段模式的简略架构计划。(图片来自于 Tinyid 的官网 wiki:《Tinyid 原理介绍》)

在这种架构模式下,咱们通过 HTTP 申请向发号器服务申请惟一 ID。负载平衡 router 会把咱们的申请送往其中的一台 tinyid-server。

这种计划有什么问题呢?在我看来(Tinyid 官网 wiki 也有介绍到),次要由上面这 2 个问题:

  • 获取新号段的状况下,程序获取惟一 ID 的速度比较慢。
  • 须要保障 DB 高可用,这个是比拟麻烦且消耗资源的。

除此之外,HTTP 调用也存在网络开销。

Tinyid 的原理比较简单,其架构如下图所示:

相比于基于数据库号段模式的简略架构计划,Tinyid 计划次要做了上面这些优化:

  • 双号段缓存:为了防止在获取新号段的状况下,程序获取惟一 ID 的速度比较慢。Tinyid 中的号段在用到肯定水平的时候,就会去异步加载下一个号段,保障内存中始终有可用号段。
  • 减少多 db 反对:反对多个 DB,并且,每个 DB 都能生成惟一 ID,进步了可用性。
  • 减少 tinyid-client:纯本地操作,无 HTTP 申请耗费,性能和可用性都有很大晋升。

Tinyid 的优缺点这里就不剖析了,联合数据库号段模式的优缺点和 Tinyid 的原理就能晓得。

分布式 ID 生成计划总结

这篇文章中,我基本上曾经把最常见的分布式 ID 生成计划都总结了一波。

后记

最初再举荐一个十分不错的 Java 教程类开源我的项目:JavaGuide。我在大三开始筹备秋招面试的时候,创立了 JavaGuide 这个我的项目。目前这个我的项目曾经有 100k+ 的 star,相干浏览:《1049 天,100K! 简略复盘!》。

对于你学习 Java 以及筹备 Java 方向的面试都很有帮忙!正如作者说的那样,这是一份:涵盖大部分 Java 程序员所须要把握的外围常识的 Java 学习 + 面试指南!

相干举荐:

  • 图解计算机根底!
  • 阿里 ACM 大佬开源的学习笔记!TQL!
  • 计算机优质书籍收罗 + 学习路线举荐!

我是 Guide 哥,拥抱开源,喜爱烹饪。开源我的项目 JavaGuide 作者,Github:Snailclimb – Overview。将来几年,心愿继续欠缺 JavaGuide,争取可能帮忙更多学习 Java 的小伙伴!共勉!凎!点击查看我的 2020 年工作汇报!

除了下面介绍的形式之外,像 ZooKeeper 这类中间件也能够帮忙咱们生成惟一 ID。没有银弹,肯定要结合实际我的项目来抉择最适宜本人的计划。

正文完
 0