|
| 1 | +## 分布式 ID |
| 2 | + |
| 3 | +### 何为 ID? |
| 4 | + |
| 5 | +日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应 |
| 10 | + |
| 11 | +简单来说,**ID 就是数据的唯一标识**。 |
| 12 | + |
| 13 | +### 何为分布式 ID? |
| 14 | + |
| 15 | +分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。 |
| 16 | + |
| 17 | +我简单举一个分库分表的例子。 |
| 18 | + |
| 19 | +我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。 |
| 20 | + |
| 21 | +单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。 |
| 22 | + |
| 23 | +在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。**我们如何为不同的数据节点生成全局唯一主键呢?** |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +这个时候就需要生成**分布式 ID**了。 |
| 28 | + |
| 29 | +### 分布式 ID 需要满足哪些要求? |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | +分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。 |
| 34 | + |
| 35 | +一个最基本的分布式 ID 需要满足下面这些要求: |
| 36 | + |
| 37 | +- **全局唯一** :ID 的全局唯一性肯定是首先要满足的! |
| 38 | +- **高性能** : 分布式 ID 的生成速度要快,对本地资源消耗要小。 |
| 39 | +- **高可用** :生成分布式 ID 的服务要保证可用性无限接近于 100%。 |
| 40 | +- **方便易用** :拿来即用,使用方便,快速接入! |
| 41 | + |
| 42 | +除了这些之外,一个比较好的分布式 ID 还应保证: |
| 43 | + |
| 44 | +- **安全** :ID 中不包含敏感信息。 |
| 45 | +- **有序递增** :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。 |
| 46 | +- **有具体的业务含义** :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。 |
| 47 | +- **独立部署** :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。 |
| 48 | + |
| 49 | +## 分布式 ID 常见解决方案 |
| 50 | + |
| 51 | +### 数据库 |
| 52 | + |
| 53 | +#### 数据库主键自增 |
| 54 | + |
| 55 | +这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。 |
| 56 | + |
| 57 | + |
| 58 | + |
| 59 | +以 MySQL 举例,我们通过下面的方式即可。 |
| 60 | + |
| 61 | +**1.创建一个数据库表。** |
| 62 | + |
| 63 | +```sql |
| 64 | +CREATE TABLE `sequence_id` ( |
| 65 | + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| 66 | + `stub` char(10) NOT NULL DEFAULT '', |
| 67 | + PRIMARY KEY (`id`), |
| 68 | + UNIQUE KEY `stub` (`stub`) |
| 69 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| 70 | +``` |
| 71 | + |
| 72 | +`stub` 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 `stub` 字段创建了唯一索引,保证其唯一性。 |
| 73 | + |
| 74 | +**2.通过 `replace into` 来插入数据。** |
| 75 | + |
| 76 | +```java |
| 77 | +BEGIN; |
| 78 | +REPLACE INTO sequence_id (stub) VALUES ('stub'); |
| 79 | +SELECT LAST_INSERT_ID(); |
| 80 | +COMMIT; |
| 81 | +``` |
| 82 | + |
| 83 | +插入数据这里,我们没有使用 `insert into` 而是使用 `replace into` 来插入数据,具体步骤是这样的: |
| 84 | + |
| 85 | +1)第一步: 尝试把数据插入到表中。 |
| 86 | + |
| 87 | +2)第二步: 如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。 |
| 88 | + |
| 89 | +这种方式的优缺点也比较明显: |
| 90 | + |
| 91 | +- **优点** :实现起来比较简单、ID 有序递增、存储消耗空间小 |
| 92 | +- **缺点** : 支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢) |
| 93 | + |
| 94 | +#### 数据库号段模式 |
| 95 | + |
| 96 | +数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。 |
| 97 | + |
| 98 | +如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 **基于数据库的号段模式来生成分布式 ID。** |
| 99 | + |
| 100 | +数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的[Tinyid](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D) 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。 |
| 101 | + |
| 102 | +以 MySQL 举例,我们通过下面的方式即可。 |
| 103 | + |
| 104 | +**1.创建一个数据库表。** |
| 105 | + |
| 106 | +```sql |
| 107 | +CREATE TABLE `sequence_id_generator` ( |
| 108 | + `id` int(10) NOT NULL, |
| 109 | + `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id', |
| 110 | + `step` int(10) NOT NULL COMMENT '号段的长度', |
| 111 | + `version` int(20) NOT NULL COMMENT '版本号', |
| 112 | + `biz_type` int(20) NOT NULL COMMENT '业务类型', |
| 113 | + PRIMARY KEY (`id`) |
| 114 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| 115 | +``` |
| 116 | + |
| 117 | +`current_max_id` 字段和`step`字段主要用于获取批量 ID,获取的批量 id 为: `current_max_id ~ current_max_id+step`。 |
| 118 | + |
| 119 | + |
| 120 | + |
| 121 | +`version` 字段主要用于解决并发问题(乐观锁),`biz_type` 主要用于表示业余类型。 |
| 122 | + |
| 123 | +**2.先插入一行数据。** |
| 124 | + |
| 125 | +```sql |
| 126 | +INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`) |
| 127 | +VALUES |
| 128 | + (1, 0, 100, 0, 101); |
| 129 | +``` |
| 130 | + |
| 131 | +**3.通过 SELECT 获取指定业务下的批量唯一 ID** |
| 132 | + |
| 133 | +```sql |
| 134 | +SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101 |
| 135 | +``` |
| 136 | + |
| 137 | +结果: |
| 138 | + |
| 139 | +``` |
| 140 | +id current_max_id step version biz_type |
| 141 | +1 0 100 1 101 |
| 142 | +``` |
| 143 | + |
| 144 | +**4.不够用的话,更新之后重新 SELECT 即可。** |
| 145 | + |
| 146 | +```sql |
| 147 | +UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0 AND `biz_type` = 101 |
| 148 | +SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101 |
| 149 | +``` |
| 150 | + |
| 151 | +结果: |
| 152 | + |
| 153 | +``` |
| 154 | +id current_max_id step version biz_type |
| 155 | +1 100 100 1 101 |
| 156 | +``` |
| 157 | + |
| 158 | +相比于数据库主键自增的方式,**数据库的号段模式对于数据库的访问次数更少,数据库压力更小。** |
| 159 | + |
| 160 | +另外,为了避免单点问题,你可以从使用主从模式来提高可用性。 |
| 161 | + |
| 162 | +**数据库号段模式的优缺点:** |
| 163 | + |
| 164 | +- **优点** :ID 有序递增、存储消耗空间小 |
| 165 | +- **缺点** :存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! ) |
| 166 | + |
| 167 | +#### NoSQL |
| 168 | + |
| 169 | + |
| 170 | + |
| 171 | +一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 `incr` 命令即可实现对 id 原子顺序递增。 |
| 172 | + |
| 173 | +```bash |
| 174 | +127.0.0.1:6379> set sequence_id_biz_type 1 |
| 175 | +OK |
| 176 | +127.0.0.1:6379> incr sequence_id_biz_type |
| 177 | +(integer) 2 |
| 178 | +127.0.0.1:6379> get sequence_id_biz_type |
| 179 | +"2" |
| 180 | +``` |
| 181 | + |
| 182 | +为了提高可用性和并发,我们可以使用 Redis Cluser。Redis Cluser 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。 |
| 183 | + |
| 184 | +除了 Redis Cluser 之外,你也可以使用开源的 Redis 集群方案[Codis](https://github.com/CodisLabs/codis) (大规模集群比如上百个节点的时候比较推荐)。 |
| 185 | + |
| 186 | +除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:**快照(snapshotting,RDB)**、**只追加文件(append-only file, AOF)**。 并且,Redis 4.0 开始支持 **RDB 和 AOF 的混合持久化**(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 |
| 187 | + |
| 188 | +关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 [JavaGuide 对于 Redis 知识点的总结](https://snailclimb.gitee.io/javaguide/#/docs/database/Redis/redis-all)。 |
| 189 | + |
| 190 | +**Redis 方案的优缺点:** |
| 191 | + |
| 192 | +- **优点** : 性能不错并且生成的 ID 是有序递增的 |
| 193 | +- **缺点** : 和数据库主键自增方案的缺点类似 |
| 194 | + |
| 195 | +除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。 |
| 196 | + |
| 197 | + |
| 198 | + |
| 199 | +MongoDB ObjectId 一共需要 12 个字节存储: |
| 200 | + |
| 201 | +- 0~3:时间戳 |
| 202 | +- 3~6: 代表机器 ID |
| 203 | +- 7~8:机器进程 ID |
| 204 | +- 9~11 :自增值 |
| 205 | + |
| 206 | +**MongoDB 方案的优缺点:** |
| 207 | + |
| 208 | +- **优点** : 性能不错并且生成的 ID 是有序递增的 |
| 209 | +- **缺点** : 需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) 、有安全性问题(ID 生成有规律性) |
| 210 | + |
| 211 | +### 算法 |
| 212 | + |
| 213 | +#### UUID |
| 214 | + |
| 215 | +UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。 |
| 216 | + |
| 217 | +JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。 |
| 218 | + |
| 219 | +```java |
| 220 | +//输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa |
| 221 | +UUID.randomUUID() |
| 222 | +``` |
| 223 | + |
| 224 | +[RFC 4122](https://tools.ietf.org/html/rfc4122) 中关于 UUID 的示例是这样的: |
| 225 | + |
| 226 | + |
| 227 | + |
| 228 | +我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。 |
| 229 | + |
| 230 | +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)): |
| 231 | + |
| 232 | +- **版本 1** : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成; |
| 233 | +- **版本 2** : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成; |
| 234 | +- **版本 3、版本 5** : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成; |
| 235 | +- **版本 4** : UUID 使用[随机性](https://zh.wikipedia.org/wiki/随机性)或[伪随机性](https://zh.wikipedia.org/wiki/伪随机性)生成。 |
| 236 | + |
| 237 | +下面是 Version 1 版本下生成的 UUID 的示例: |
| 238 | + |
| 239 | + |
| 240 | + |
| 241 | +JDK 中通过 `UUID` 的 `randomUUID()` 方法生成的 UUID 的版本默认为 4。 |
| 242 | + |
| 243 | +```java |
| 244 | +UUID uuid = UUID.randomUUID(); |
| 245 | +int version = uuid.version();// 4 |
| 246 | +``` |
| 247 | + |
| 248 | +另外,Variant(变体)也有 4 种不同的值,这种值分别对应不同的含义。这里就不介绍了,貌似平时也不怎么需要关注。 |
| 249 | + |
| 250 | +需要用到的时候,去看看维基百科对于 UUID 的 Variant(变体) 相关的介绍即可。 |
| 251 | + |
| 252 | +从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。 |
| 253 | + |
| 254 | +虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。 |
| 255 | + |
| 256 | +比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适: |
| 257 | + |
| 258 | +- 数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。 |
| 259 | +- UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。 |
| 260 | + |
| 261 | +最后,我们再简单分析一下 **UUID 的优缺点** (面试的时候可能会被问到的哦!) : |
| 262 | + |
| 263 | +- **优点** :生成速度比较快、简单易用 |
| 264 | +- **缺点** : 存储消耗空间大(32 个字符串,128 位) 、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) |
| 265 | + |
| 266 | +#### Snowflake(雪花算法) |
| 267 | + |
| 268 | +Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义: |
| 269 | + |
| 270 | +- **第 0 位**: 符号位(标识正负),始终为 0,没有用,不用管。 |
| 271 | +- **第 1~41 位** :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年) |
| 272 | +- **第 42~52 位** :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。 |
| 273 | +- **第 53~64 位** :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。 |
| 274 | + |
| 275 | + |
| 276 | + |
| 277 | +如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。 |
| 278 | + |
| 279 | +另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。 |
| 280 | + |
| 281 | +我们再来看看 Snowflake 算法的优缺点 : |
| 282 | + |
| 283 | +- **优点** :生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID) |
| 284 | +- **缺点** : 需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。 |
| 285 | + |
| 286 | +### 开源框架 |
| 287 | + |
| 288 | +#### UidGenerator(百度) |
| 289 | + |
| 290 | +[UidGenerator](https://github.com/baidu/uid-generator) 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。 |
| 291 | + |
| 292 | +不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下。 |
| 293 | + |
| 294 | + |
| 295 | + |
| 296 | +可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。 |
| 297 | + |
| 298 | +UidGenerator 官方文档中的介绍如下: |
| 299 | + |
| 300 | + |
| 301 | + |
| 302 | +自 18 年后,UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 [UidGenerator 的官方介绍](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md)。 |
| 303 | + |
| 304 | +#### Leaf(美团) |
| 305 | + |
| 306 | +**[Leaf](https://github.com/Meituan-Dianping/Leaf)** 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话: “There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了! |
| 307 | + |
| 308 | + |
| 309 | + |
| 310 | +Leaf 提供了 **号段模式** 和 **Snowflake(雪花算法)** 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。 |
| 311 | + |
| 312 | +Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。 |
| 313 | + |
| 314 | +Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:[《Leaf——美团点评分布式 ID 生成系统》](https://tech.meituan.com/2017/04/21/mt-leaf.html))。 |
| 315 | + |
| 316 | + |
| 317 | + |
| 318 | +根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。 |
| 319 | + |
| 320 | +#### Tinyid(滴滴) |
| 321 | + |
| 322 | +[Tinyid](https://github.com/didi/tinyid) 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。 |
| 323 | + |
| 324 | +数据库号段模式的原理我们在上面已经介绍过了。**Tinyid 有哪些亮点呢?** |
| 325 | + |
| 326 | +为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:[《Tinyid 原理介绍》](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D)) |
| 327 | + |
| 328 | + |
| 329 | + |
| 330 | +在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。 |
| 331 | + |
| 332 | +这种方案有什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题: |
| 333 | + |
| 334 | +- 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 |
| 335 | +- 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。 |
| 336 | + |
| 337 | +除此之外,HTTP 调用也存在网络开销。 |
| 338 | + |
| 339 | +Tinyid 的原理比较简单,其架构如下图所示: |
| 340 | + |
| 341 | + |
| 342 | + |
| 343 | +相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化: |
| 344 | + |
| 345 | +- **双号段缓存** :为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。 |
| 346 | +- **增加多 db 支持** :支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。 |
| 347 | +- **增加 tinyid-client** :纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。 |
| 348 | + |
| 349 | +Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。 |
| 350 | + |
| 351 | +## 分布式 ID 生成方案总结 |
| 352 | + |
| 353 | +这篇文章中,我基本上已经把最常见的分布式 ID 生成方案都总结了一波。 |
| 354 | + |
| 355 | +除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。**没有银弹,一定要结合实际项目来选择最适合自己的方案。** |
0 commit comments