🍁 作者:知识浅谈,阿里云技术博主,CSDN签约讲师,后端领域优质创作者,热爱分享创作
💒 公众号:知识浅谈
📌 擅长领域:后端全栈工程师、爬虫、ACM算法
🔥 联系方式vx:zsqtcc
她把分布式 ID 常见解决方案讲的真的透彻。
🤞这次都给他拿下🤞
为什么 分布式 ID 使用这么频繁呢? 这主要是因为大数据量,高并发使得单体数据库显得力不从心了。
正菜来了🛴🛴🛴
🍖基于sql数据库方案
🍕数据库主键自增
这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。
以 MySQL 举例,我们通过下面的方式即可。
🥖.创建一个数据库表。
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 字段创建了唯一索引,保证其唯一性。
🥖.通过 replace into 来插入数据。
BEGIN; REPLACE INTO sequence_id (stub) VALUES ('stub'); SELECT LAST_INSERT_ID(); COMMIT;
插入数据这里,我们没有使用 insert into 而是使用 replace into 来插入数据,具体步骤是这样的这里产生id的方法是插入的时候如果主键是自增的,insert的时候会返回自动生成的id:
- 第一步: 尝试把数据插入到表中。
- 第二步: 如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。
这种方式的优缺点也比较明显:
优点 :实现起来比较简单、ID 有序递增、存储消耗空间小
缺点 : 支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)
🍕数据库号段模式
数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。
如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 基于数据库的号段模式来生成分布式 ID。
数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的Tinyid 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。
以 MySQL 举例,我们通过下面的方式即可。
🥖创建一个数据库表。
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 主要用于表示业务类型。
🥖先插入一行数据。
INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`) VALUES (1, 0, 100, 0, 101);
🥖通过 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
🥖不够用的话,更新之后重新 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 Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。
除了 Redis Cluster 之外,你也可以使用开源的 Redis 集群方案Codis (大规模集群比如上百个节点的时候比较推荐)。
除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:快照(snapshotting,RDB)、只追加文件(append-only file, AOF)。 并且,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
Redis 方案的优缺点:
优点 : 性能不错并且生成的 ID 是有序递增的
缺点 : 和数据库主键自增方案的缺点类似
🍖基于UUID的解决方案
UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。
JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。
//输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa UUID.randomUUID()
我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。
5 种不同的 Version(版本)值分别对应的含义:
- 版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
- 版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
- 版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成;
- 版本 4 : UUID 使用随机性或伪随机性生成。
JDK 中通过 UUID 的 randomUUID() 方法生成的 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 是 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{指的是时钟回拨问题})
关于时钟回拨的解决思路
✨不依赖机器时钟驱动,就没时钟回拨的事儿了。即定义一个初始时间戳,在初始时间戳上自增,不跟随机器时钟增加。时间戳何时自增?当序列号增加到最大时,此时时间戳+1,这样完全不会浪费序列号,适合流量较大的场景,如果流量较小,可能出现时间断层滞后。
✨依然依赖机器时钟,如果时钟回拨范围较小,如几十毫秒,可以等到时间回到正常;如果流量不大,前几百毫秒或者几秒的序列号肯定有剩余,可以将前几百毫秒或者几秒的序列号缓存起来,如果发生时钟回拨,就从缓存中获取序列号自增。
🍚总结
除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。一定要结合实际项目来选择最适合自己的方案。