本文介绍: 日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示比如用户 ID 对应且仅对应个人商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人地址 ID 对应且仅对应简单来说,ID 就是数据唯一标识分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。我简单一个分库分表例子。我司的一个项目使用的是单机 MySQL。

分布式 ID 介绍

什么是 ID?

日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单

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

简单来说,ID 就是数据唯一标识

什么分布式 ID?

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

简单举一个分库分表的例子

我司的一个项目使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统数据量将越来越大。单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。

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

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

img

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

img

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

一个最基本的分布式 ID 需要满足下面这些要求:

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

分布式 ID 常见解决方案

数据库

数据库主键自增

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

数据库主键自增

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插入数据,具体步骤是这样的:

这种方式的优缺点也比较明显:

数据库号段模式

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

如果我们可以批量获取,然后存在内存里面,需要用到时候直接从内存里面拿就舒服了!这也就是我们说的 基于数据库的号段模式来生成分布式 ID。

数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源Tinyidopen in new window 就是基于这种方式来做的。不过,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 0 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

img

一般情况下,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 Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。

除了 Redis Cluster 之外,你也可以使用开源的 Redis 集群方案Codisopen in new window (大规模集群比如上百个节点时候比较推荐)。

除了高可用并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同持久化方式:快照snapshotting,RDB)追加文件appendonly file, AOF)。 并且,Redis 4.0 开始支持 RDB 和 AOF 的混合持久默认关闭,可以通过配置aof-use-rdb-preamble 开启)。

Redis 方案的优缺点:

  • 优点性能不错并且生成的 ID 是有序递增的
  • 缺点:和数据库主键自增方案的缺点类似

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

img

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

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()

从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址时间戳、名字空间(Namespace)、随机或伪随机数时序元素计算基于这些规则生成的 UUID 是肯定不会重复的。

虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。

比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适:

最后,我们再简单分析一下 UUID 的优缺点面试的时候可能会被问到的哦!) :

Snowflake(雪花算法)

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

Snowflake 组成Snowflake 组成

在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息

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

  • 优点:生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)
  • 缺点:需要解决重复 ID 问题(ID 生成依赖时间,在获取时间的时候,可能出现时间回拨的问题,也就是服务器上的时间突然倒退到之前的时间,进而导致会产生重复 ID)、依赖机器 ID 对分布式环境不友好(当需要自动启停或增减机器时,固定的机器 ID 可能不够灵活)。

如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法开源实现比如美团 的 Leaf百度的 UidGenerator(后面会提到),并且这些开源实现对原有的 Snowflake 算法进行了优化,性能更优秀,还解决了 Snowflake 算法的时间回拨问题和依赖机器 ID 的问题。

并且,Seata 还提出了“改良版雪花算法”,针对原版雪花算法进行了一定的优化改良,解决了时间回拨问题,大幅提高的 QPS。

开源框架

UidGenerator(百度)

UidGeneratoropen in new window百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器

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

UidGenerator 生成的 ID 组成

可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义

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

img

Leaf(美团)

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

Leaf 提供了 号段模式Snowflake(雪花算法) 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper(使用 Zookeeper 作为注册中心,通过在特定路径读取创建节点管理 workId) 。

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

Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章

img

根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms

Tinyid(滴滴)

Tinyidopen in new window滴滴开源的一款基于数据库号段模式的唯一 ID 生成器

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

为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案

img

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

这种方案什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题:

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

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

img

相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化:

IdGenerator(个人)

和 UidGenerator、Leaf 一样,IdGeneratoropen in new window 也是一款基于 Snowflake(雪花算法)的唯一 ID 生成器

IdGenerator 有如下特点:

IdGenerator 生成的唯一 ID 组成如下:

IdGenerator 生成的 ID 组成

timestamp (位数固定):时间差,是生成 ID 时的系统时间减去 BaseTime(基础时间,也称基点时间、原点时间、纪元时间,默认值为 2020 年) 的总时间差(毫秒单位)。初始为 5bits,随着运行时间而增加。如果觉得默认值太老,你可以重新设置,不过要注意这个值以后最好不变。

作者声明

如有问题,欢迎指正!

原文地址:https://blog.csdn.net/weixin_45247019/article/details/134814468

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_48430.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注