1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-20 22:17:09 +08:00

[feat]分布式ID文章完善更新

This commit is contained in:
guide 2021-09-23 11:43:51 +08:00
parent 7ec4dcd7d4
commit 1947e5a1e5
3 changed files with 356 additions and 185 deletions

View File

@ -278,7 +278,7 @@ Dubbo 是一款国产的 RPC 框架,由阿里开源。相关阅读:
#### 分布式 id
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。比如数据量太大之后,往往需要对数据进行分库分表,分库分表后需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求。相关阅读:[为什么要分布式 id ?分布式 id 生成方案有哪些?](docs/system-design/micro-service/分布式id生成方案总结.md)
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。比如数据量太大之后,往往需要对数据进行分库分表,分库分表后需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求。相关阅读:[为什么要分布式 id ?分布式 id 生成方案有哪些?](docs/system-design/distributed/分布式ID.md)
#### 分布式事务

View File

@ -0,0 +1,355 @@
## 分布式 ID
### 何为 ID
日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/up-79beb853b8319f850638c9708f83039dfda.png)
我们现实生活中也有各种 ID比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应
简单来说,**ID 就是数据的唯一标识**。
### 何为分布式 ID
分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。
我简单举一个分库分表的例子。
我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。
单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC
在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。**我们如何为不同的数据节点生成全局唯一主键呢?**
![](https://oscimg.oschina.net/oscnet/up-d78d9d5362c71f4713a090baf7ec65d2b6d.png)
这个时候就需要生成**分布式 ID**了。
### 分布式 ID 需要满足哪些要求?
![](https://img-blog.csdnimg.cn/20210610082309988.png)
分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。
一个最基本的分布式 ID 需要满足下面这些要求:
- **全局唯一** ID 的全局唯一性肯定是首先要满足的!
- **高性能** 分布式 ID 的生成速度要快,对本地资源消耗要小。
- **高可用** :生成分布式 ID 的服务要保证可用性无限接近于 100%。
- **方便易用** :拿来即用,使用方便,快速接入!
除了这些之外,一个比较好的分布式 ID 还应保证:
- **安全** ID 中不包含敏感信息。
- **有序递增** :如果要把 ID 存放在数据库的话ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。
- **有具体的业务含义** :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
- **独立部署** :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。
## 分布式 ID 常见解决方案
### 数据库
#### 数据库主键自增
这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。
![](https://img-blog.csdnimg.cn/20210610081957287.png)
以 MySQL 举例,我们通过下面的方式即可。
**1.创建一个数据库表。**
```sql
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` 来插入数据。**
```java
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](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D) 就是基于这种方式来做的。不过TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。
以 MySQL 举例,我们通过下面的方式即可。
**1.创建一个数据库表。**
```sql
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`
![](https://img-blog.csdnimg.cn/20210610081149228.png)
`version` 字段主要用于解决并发问题(乐观锁),`biz_type` 主要用于表示业余类型。
**2.先插入一行数据。**
```sql
INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES
(1, 0, 100, 0, 101);
```
**3.通过 SELECT 获取指定业务下的批量唯一 ID**
```sql
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 即可。**
```sql
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
![](https://img-blog.csdnimg.cn/2021061008245858.png)
一般情况下NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 `incr` 命令即可实现对 id 原子顺序递增。
```bash
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](https://github.com/CodisLabs/codis) (大规模集群比如上百个节点的时候比较推荐)。
除了高可用和并发之外,我们知道 Redis 基于内存我们需要持久化数据避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:**快照snapshottingRDB**、**只追加文件append-only file, AOF**。 并且Redis 4.0 开始支持 **RDB 和 AOF 的混合持久化**(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。
关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 [JavaGuide 对于 Redis 知识点的总结](https://snailclimb.gitee.io/javaguide/#/docs/database/Redis/redis-all)。
**Redis 方案的优缺点:**
- **优点** 性能不错并且生成的 ID 是有序递增的
- **缺点** 和数据库主键自增方案的缺点类似
除了 Redis 之外MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。
![](https://img-blog.csdnimg.cn/20210207103320582.png)
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 的方法,一行代码就行了。
```java
//输出示例cb4a9ede-fa5e-4585-b9bb-d60bce986eaa
UUID.randomUUID()
```
[RFC 4122](https://tools.ietf.org/html/rfc4122) 中关于 UUID 的示例是这样的:
![](https://img-blog.csdnimg.cn/20210202110824430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70)
我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。
5 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81)
- **版本 1** : UUID 是根据时间和节点 ID通常是 MAC 地址)生成;
- **版本 2** : UUID 是根据标识符(通常是组或用户 ID、时间和节点 ID 生成;
- **版本 3、版本 5** : 版本 5 - 确定性 UUID 通过散列hashing名字空间namespace标识符和名称生成
- **版本 4** : UUID 使用[随机性](https://zh.wikipedia.org/wiki/随机性)或[伪随机性](https://zh.wikipedia.org/wiki/伪随机性)生成。
下面是 Version 1 版本下生成的 UUID 的示例:
![](https://img-blog.csdnimg.cn/20210202113013477.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70)
JDK 中通过 `UUID``randomUUID()` 方法生成的 UUID 的版本默认为 4。
```java
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。
![](https://oscimg.oschina.net/oscnet/up-a7e54a77b5ab1d9fa16d5ae3a3c50c5aee9.png)
如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator并且这些开源实现对原有的 Snowflake 算法进行了优化。
另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。
我们再来看看 Snowflake 算法的优缺点
- **优点** :生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID
- **缺点** 需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID
### 开源框架
#### UidGenerator(百度)
[UidGenerator](https://github.com/baidu/uid-generator) 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。
不过UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下。
![](https://oscimg.oschina.net/oscnet/up-ad5b9dd0077a949db923611b2450277e406.png)
可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。
UidGenerator 官方文档中的介绍如下:
![](https://oscimg.oschina.net/oscnet/up-358b1a4cddb3675018b8595f66ece9cae88.png)
自 18 年后UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 [UidGenerator 的官方介绍](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md)。
#### Leaf(美团)
**[Leaf](https://github.com/Meituan-Dianping/Leaf)** 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf树叶 起源于德国哲学家、数学家莱布尼茨的一句话: “There are no two identical leaves in the world”世界上没有两片相同的树叶 。这名字起得真心挺不错的,有点文艺青年那味了!
![](https://img-blog.csdnimg.cn/20210422145229617.png)
Leaf 提供了 **号段模式****Snowflake(雪花算法)** 这两种模式来生成分布式 ID。并且它支持双号段还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。
Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。
Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:[《Leaf——美团点评分布式 ID 生成系统》](https://tech.meituan.com/2017/04/21/mt-leaf.html))。
![](https://img-blog.csdnimg.cn/20210422144846724.png)
根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用QPS 压测结果近 5w/sTP999 1ms。
#### Tinyid(滴滴)
[Tinyid](https://github.com/didi/tinyid) 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。
数据库号段模式的原理我们在上面已经介绍过了。**Tinyid 有哪些亮点呢?**
为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:[《Tinyid 原理介绍》](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D)
![](https://oscimg.oschina.net/oscnet/up-4afc0e45c0c86ba5ad645d023dce11e53c2.png)
在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。
这种方案有什么问题呢在我看来Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题:
- 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。
- 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。
除此之外HTTP 调用也存在网络开销。
Tinyid 的原理比较简单,其架构如下图所示:
![](https://oscimg.oschina.net/oscnet/up-53f74cd615178046d6c04fe50513fee74ce.png)
相比于基于数据库号段模式的简单架构方案Tinyid 方案主要做了下面这些优化:
- **双号段缓存** :为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
- **增加多 db 支持** :支持多个 DB并且每个 DB 都能生成唯一 ID提高了可用性。
- **增加 tinyid-client** :纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。
Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。
## 分布式 ID 生成方案总结
这篇文章中,我基本上已经把最常见的分布式 ID 生成方案都总结了一波。
除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。**没有银弹,一定要结合实际项目来选择最适合自己的方案。**

View File

@ -1,184 +0,0 @@
> 点击关注[公众号](#公众号)及时获取笔主最新更新文章并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
>
> 本文授权转载自https://juejin.im/post/5d6fc8eff265da03ef7a324b 作者1点25。
ID是数据的唯一标识传统的做法是利用UUID和数据库的自增ID在互联网企业中大部分公司使用的都是Mysql并且因为需要事务支持所以通常会使用Innodb存储引擎UUID太长以及无序所以并不适合在Innodb中来作为主键自增ID比较合适但是随着公司的业务发展数据量将越来越大需要对数据进行分表而分表后每个表中的数据都会按自己的节奏进行自增很有可能出现ID冲突。这时就需要一个单独的机制来负责生成唯一ID生成出来的ID也可以叫做**分布式ID**,或**全局ID**。下面来分析各个生成分布式ID的机制。
![常用分布式id方案](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/分布式id方案.jpeg)
这篇文章并不会分析的特别详细,主要是做一些总结,以后再出一些详细某个方案的文章。
## 数据库自增ID
第一种方案仍然还是基于数据库的自增ID需要单独使用一个数据库实例在这个实例中新建一个单独的表
表结构如下:
```sql
CREATE DATABASE `SEQID`;
CREATE TABLE SEQID.SEQUENCE_ID (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM;
```
可以使用下面的语句生成并获取到一个自增ID
```sql
begin;
replace into SEQUENCE_ID (stub) VALUES ('anyword');
select last_insert_id();
commit;
```
stub字段在这里并没有什么特殊的意义只是为了方便的去插入数据只有能插入数据才能产生自增id。而对于插入我们用的是replacereplace会先看是否存在stub指定值一样的数据如果存在则先delete再insert如果不存在则直接insert。
这种生成分布式ID的机制需要一个单独的Mysql实例虽然可行但是基于性能与可靠性来考虑的话都不够**业务系统每次需要一个ID时都需要请求数据库获取性能低并且如果此数据库实例下线了那么将影响所有的业务系统。**
为了解决数据库可靠性问题我们可以使用第二种分布式ID生成方案。
## 数据库多主模式
如果我们两个数据库组成一个**主从模式**集群正常情况下可以解决数据库可靠性问题但是如果主库挂掉后数据没有及时同步到从库这个时候会出现ID重复的现象。我们可以使用**双主模式**集群也就是两个Mysql实例都能单独的生产自增ID这样能够提高效率但是如果不经过其他改造的话这两个Mysql实例很可能会生成同样的ID。需要单独给每个Mysql实例配置不同的起始值和自增步长。
第一台Mysql实例配置
```sql
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 2; -- 步长
```
第二台Mysql实例配置
```sql
set @@auto_increment_offset = 2; -- 起始值
set @@auto_increment_increment = 2; -- 步长
```
经过上面的配置后这两个Mysql实例生成的id序列如下 mysql1,起始值为1,步长为2,ID生成的序列为1,3,5,7,9,... mysql2,起始值为2,步长为2,ID生成的序列为2,4,6,8,10,...
对于这种生成分布式ID的方案需要单独新增一个生成分布式ID应用比如DistributIdService该应用提供一个接口供业务应用获取ID业务应用需要一个ID时通过rpc的方式请求DistributIdServiceDistributIdService随机去上面的两个Mysql实例中去获取ID。
实行这种方案后就算其中某一台Mysql实例下线了也不会影响DistributIdServiceDistributIdService仍然可以利用另外一台Mysql来生成ID。
但是这种方案的扩展性不太好如果两台Mysql实例不够用需要新增Mysql实例来提高性能时这时就会比较麻烦。
现在如果要新增一个实例mysql3要怎么操作呢 第一mysql1、mysql2的步长肯定都要修改为3而且只能是人工去修改这是需要时间的。 第二因为mysql1和mysql2是不停在自增的对于mysql3的起始值我们可能要定得大一点以给充分的时间去修改mysql1mysql2的步长。 第三在修改步长的时候很可能会出现重复ID要解决这个问题可能需要停机才行。
为了解决上面的问题以及能够进一步提高DistributIdService的性能如果使用第三种生成分布式ID机制。
## 号段模式
我们可以使用号段的方式来获取自增ID号段可以理解成批量获取比如DistributIdService从数据库获取ID时如果能批量获取多个ID并缓存在本地的话那样将大大提供业务应用获取ID的效率。
比如DistributIdService每次从数据库获取ID时就获取一个号段比如(1,1000]这个范围表示了1000个ID业务应用在请求DistributIdService提供ID时DistributIdService只需要在本地从1开始自增并返回即可而不需要每次都请求数据库一直到本地自增到1000时也就是当前号段已经被用完时才去数据库重新获取下一号段。
所以,我们需要对数据库表进行改动,如下:
```sql
CREATE TABLE id_generator (
id int(10) NOT NULL,
current_max_id bigint(20) NOT NULL COMMENT '当前最大id',
increment_step int(10) NOT NULL COMMENT '号段的长度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
这个数据库表用来记录自增步长以及当前自增ID的最大值也就是当前已经被申请的号段的最后一个值因为自增逻辑被移到DistributIdService中去了所以数据库不需要这部分逻辑了。
这种方案不再强依赖数据库就算数据库不可用那么DistributIdService也能继续支撑一段时间。但是如果DistributIdService重启会丢失一段ID导致ID空洞。
为了提高DistributIdService的高可用需要做一个集群业务在请求DistributIdService集群获取ID时会随机的选择某一个DistributIdService节点进行获取对每一个DistributIdService节点来说数据库连接的是同一个数据库那么可能会产生多个DistributIdService节点同时请求数据库获取号段那么这个时候需要利用乐观锁来进行控制比如在数据库表中增加一个version字段在获取号段时使用如下SQL
```sql
update id_generator set current_max_id=#{newMaxId}, version=version+1 where version = #{version}
```
因为newMaxId是DistributIdService中根据oldMaxId+步长算出来的只要上面的update更新成功了就表示号段获取成功了。
为了提供数据库层的高可用需要对数据库使用多主模式进行部署对于每个数据库来说要保证生成的号段不重复这就需要利用最开始的思路再在刚刚的数据库表中增加起始值和步长比如如果现在是两台Mysql那么 mysql1将生成号段1,1001]自增的时候序列为1357.... mysql1将生成号段2,1002]自增的时候序列为246810...
更详细的可以参考滴滴开源的TinyId[github.com/didi/tinyid…](https://github.com/didi/tinyid/wiki/tinyid原理介绍)
在TinyId中还增加了一步来提高效率在上面的实现中ID自增的逻辑是在DistributIdService中实现的而实际上可以把自增的逻辑转移到业务应用本地这样对于业务应用来说只需要获取号段每次自增时不再需要请求调用DistributIdService了。
## 雪花算法
上面的三种方法总的来说是基于自增思想的,而接下来就介绍比较著名的雪花算法-snowflake。
我们可以换个角度来对分布式ID进行思考只要能让负责生成分布式ID的每台机器在每毫秒内生成不一样的ID就行了。
snowflake是twitter开源的分布式ID生成算法是一种算法所以它和上面的三种生成分布式ID机制不太一样它不依赖数据库。
核心思想是分布式ID固定是一个long型的数字一个long型占8个字节也就是64个bit原始snowflake算法中对于bit的分配如下图
![雪花算法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/雪花算法.png)
- 第一个bit位是标识部分在java中由于long的最高位是符号位正数是0负数是1一般生成的ID为正数所以固定为0。
- 时间戳部分占41bit这个是毫秒级的时间一般实现上不会存储当前的时间戳而是时间戳的差值当前时间-固定的开始时间这样可以使产生的ID从更小值开始41位的时间戳可以使用69年(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
- 工作机器id占10bit这里比较灵活比如可以使用前5位作为数据中心机房标识后5位作为单机房机器标识可以部署1024个节点。
- 序列号部分占12bit支持同一毫秒内同一个节点可以生成4096个ID
根据这个算法的逻辑只需要将这个算法用Java语言实现出来封装为一个工具方法那么各个业务应用可以直接使用该工具方法来获取分布式ID只需保证每个业务应用有自己的工作机器id即可而不需要单独去搭建一个获取分布式ID的应用。
snowflake算法实现起来并不难提供一个github上用java实现的[github.com/beyondfengy…](https://github.com/beyondfengyu/SnowFlake)
在大厂里其实并没有直接使用snowflake而是进行了改造因为snowflake算法中最难实践的就是工作机器id原始的snowflake算法需要人工去为每台机器去指定一个机器id并配置在某个地方从而让snowflake从此处获取机器id。
但是在大厂里机器是很多的人力成本太大且容易出错所以大厂对snowflake进行了改造。
### 百度uid-generator
github地址[uid-generator](https://github.com/baidu/uid-generator)
uid-generator使用的就是snowflake只是在生产机器id也叫做workId时有所不同。
uid-generator中的workId是由uid-generator自动生成的并且考虑到了应用部署在docker上的情况在uid-generator中用户可以自己去定义workId的生成策略默认提供的策略是应用启动时由数据库分配。说的简单一点就是应用在启动时会往数据库表(uid-generator需要新增一个WORKER_NODE表)中去插入一条数据数据插入成功后返回的该数据对应的自增唯一id就是该机器的workId而数据由hostport组成。
对于uid-generator中的workId占用了22个bit位时间占用了28个bit位序列化占用了13个bit位需要注意的是和原始的snowflake不太一样时间的单位是秒而不是毫秒workId也不一样同一个应用每重启一次就会消费一个workId。
具体可参考[github.com/baidu/uid-g…](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md)
### 美团Leaf
github地址[Leaf](https://github.com/Meituan-Dianping/Leaf)
美团的Leaf也是一个分布式ID生成框架。它非常全面即支持号段模式也支持snowflake模式。号段模式这里就不介绍了和上面的分析类似。
Leaf中的snowflake模式和原始snowflake算法的不同点也主要在workId的生成Leaf中workId是基于ZooKeeper的顺序Id来生成的每个应用在使用Leaf-snowflake时在启动时都会都在Zookeeper中生成一个顺序Id相当于一台机器对应一个顺序节点也就是一个workId。
### 总结
总得来说上面两种都是自动生成workId以让系统更加稳定以及减少人工成本。
## Redis
这里额外再介绍一下使用Redis来生成分布式ID其实和利用Mysql自增ID类似可以利用Redis中的incr命令来实现原子性的自增与返回比如
```shell
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1并返回
(integer) 2
127.0.0.1:6379> incr seq_id // 增加1并返回
(integer) 3
```
使用redis的效率是非常高的但是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。
RDB持久化相当于定时打一个快照进行持久化如果打完快照后连续自增了几次还没来得及做下一次快照持久化这个时候Redis挂掉了重启Redis后会出现ID重复。
AOF持久化相当于对每条写命令进行持久化如果Redis挂掉了不会出现ID重复的现象但是会由于incr命令过多导致重启恢复数据时间过长。
## 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)