「尺有所短,寸有所长;不忘初心,方得始终」。
在业务开发中,大量场景需要唯一 ID 来进行标识:用户需要唯一身份标识、商品需要唯一标识、消息需要唯一标识、事件需要唯一标识等,都需要全局唯一ID,尤其是复杂的分布式业务场景中全局唯一 ID 更为重要。
-
「那么,分布式唯一 ID 有哪些特性或要求呢?」
-
唯一性:生成的 ID 全局唯一,在特定范围内冲突概率极小。 -
有序性:生成的 ID 按某种规则有序,便于数据库插入及排序。 -
可用性:可保证高并发下的可用性, 确保任何时候都能正确的生成 ID。 -
自主性:分布式环境下不依赖中心认证即可自行生成 ID。 -
安全性:不暴露系统和业务的信息, 如:订单数,用户数等。 -
「分布式唯一 ID 有哪些生成方法呢?」
-
「UUID 生成」
-
「数据库自增 ID」
-
「snowflake雪花算法」
-
「Redis的Incr命令获取全局唯⼀ID」
另外,⼀些互联⽹公司也基于上述的⽅案封装了⼀些分布式ID⽣成器,⽐如滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)等
一、 UUID 生成
「核心思想:结合机器的网卡(基于名字空间/名字的散列值 MD5/SHA1)、当地时间(基于时间戳&时钟序列)、一个随记数来生成 UUID。」其结构:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee (包含 32 个 16 进制数字,以连字号-分为五段,最终形成“8-4-4-4-12”的 36 个字符的字符串,即 32 个英数字母+4 个连字号)。例如:550e8400-e29b-41d4-a716-446655440000
-
「优点:」 -
本地生成,没有网络消耗,生成简单,没有高可用风险。 -
「缺点:」 -
不易于存储:UUID 太长, 16 字节 128 位,通常以 36 长度的字符串表示,很多场景不适用。 -
信息不安全:基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。 -
无序查询效率低:由于生成的 UUID 是无序不可读的字符串,所以其查询效率低。

二、 数据库自增 ID
「核心思想:使用数据库的 id 自增策略(如: Mysql 的 auto_increment)。」
案例:
⽐如A表分表为A1表和A2表,那么肯定不能让A1表和A2表的ID⾃增,那么ID怎么获取呢?我们可 以单独的创建⼀个Mysql数据库,在这个数据库中创建⼀张表,这张表的ID设置为⾃增,其他地⽅ 需要全局唯⼀ID的时候,就模拟向这个Mysql数据库的这张表中模拟插⼊⼀条记录,此时ID会⾃ 增,然后我们可以通过Mysql的select last_insert_id() 获取到刚刚这张表中⾃增⽣成的ID.
⽐如,我们创建了⼀个数据库实例global_id_generator,在其中创建了⼀个数据表,表结构如 下:
DROP TABLE IF EXISTS `DISTRIBUTE_ID`;
CREATE TABLE `DISTRIBUTE_ID` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`createtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;「当分布式集群环境中哪个应⽤需要获取⼀个全局唯⼀的分布式ID的时候,就可以使⽤代码连接这个」「数据库实例,执⾏如下sql语句即可」。
insert into DISTRIBUTE_ID(createtime) values(NOW());
select LAST_INSERT_ID();注意:
1)这⾥的createtime字段⽆实际意义,是为了随便插⼊⼀条数据以⾄于能够⾃增id。
2)使⽤独⽴的Mysql实例⽣成分布式id,虽然可⾏,但是性能和可靠性都不够好,因为你需要代码连接到数据库才能获取到id,性能⽆法保障,另外mysql数据库实例挂掉了,那么就⽆法获取分布式id了。
3)有⼀些开发者⼜针对上述的情况将⽤于⽣成分布式id的mysql数据库设计成了⼀个集群架构,那么其实这种⽅式现在基本不⽤,因为过于麻烦了。
-
「优点」:简单,天然有序。 -
「缺点」: -
并发性不好。 -
数据库写压力大。 -
数据库故障后不可使用。 -
存在数量泄露风险。
针对以上缺点,有以下几种优化方案
-
「数据库水平拆分,设置不同的初始值和相同的自增步长」
「核心思想:将数据库进行水平拆分,每个数据库设置不同的初始值和相同的自增步长 。」
如图所示,「可保证每台数据库生成的 ID 是不冲突的,但这种固定步长的方式也会带来扩容的问题」,很容易想到当扩容时会出现无 ID 初始值可分的窘境。解决方案有:
-
「根据扩容考虑决定步长」。
-
「增加其他位标记区分扩容。」
这其实都是在需求与方案间的权衡,根据需求来选择最适合的方式。
-
「批量缓存自增 ID」 .
「核心思想:如果使用单台机器做 ID 生成,可以避免固定步长带来的扩容问题(方案 1 的缺点)。」具体做法是:
「每次批量生成一批 ID 给不同的机器去慢慢消费,这样数据库的压力也会减小到 N 分之一,且故障后可坚持一段时间 。」

如图所示,但这种做法的「缺点是服务器重启、单点故障会造成 ID 不连续」。「没有最好的方案,只有最适合的方案。」
三、 snowflake雪花算法
「核心思想:把 64-bit 分别划分成多段,分开来标示机器、时间、某一并发序列等,从而使每台机器及同一机器生成的 ID 都是互不相同。」
PS:这种结构是雪花算法提出者 Twitter 的分法,但实际上这种算法使用可以很灵活,根据自身业务的并发情况、机器分布、使用年限等,可以自由地重新决定各部分的位数,从而增加或减少某部分的量级。「比如:滴滴的tinyid(基于数据库实现)、百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和SnowFlake)等,都是基于雪花算法做一些适合自身业务的变化」。
其结构如下:

说明:全部结构标识(1+41+10+12=64)加起来刚好 64 位,刚好凑成一个 Long 型
分段 | 描述 |
---|---|
1 位标识 | 由于 long 基本类型在 Java 中是带符号的,最高位是符号位,正数是 0,负数是 1,所以 id 一般是正数,最高位是 0。 |
41 位时间截(毫秒级) | 需要注意的是, 41 位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 – 开始时间截)得到的值,这里的开始时间截,一般是指我们的 id 生成器开始使用的时间截,由我们的程序来指定。41 位的毫秒时间截,可以使用 69 年(即 T =(1L << 41) /(1000 * 60 * 60 *24 * 365) = 69)。 |
10 位的数据机器位 | 包括 「5位数据中心标识 Id(datacenterId)、 5 位机器标识 Id(workerId),最多可以部署 1024 个节点」(即 1 << 10 = 1024)。超过这个数量,生成的 ID 就有可能会冲突。 |
10 位的数据机器位 | 包括 5 位数据中心标识 Id(datacenterId)、 5 位机器标识 Id(workerId),最多可以部署 1024 个节点(即 1 << 10 = 1024)。超过这个数量,生成的 ID 就有可能会冲突。 |
-
「优点」:
-
整体上按照时间按时间趋势递增,后续插入索引树的时候性能较好。 -
整个分布式系统内不会产生 ID 碰撞(由数据中心标识 ID、机器标识 ID 作区分)。 -
本地生成,且不依赖数据库(或第三方组件)没有网络消耗,所以效率高(每秒能够产生 26 万 ID 左右)。 -
「缺点:」
由于雪花算法是强依赖于时间的,在分布式环境下,如果发生时钟回拨,很可能会引起 ID 重复、 ID 乱序、服务会处于不可用状态等问题。
-
「解决方案有:」
-
将 ID 生成交给少量服务器,并关闭时钟同步。 -
直接报错,交给上层业务处理。 -
如果回拨时间较短,在耗时要求内,比如 5ms,那么等待回拨时长后再进行生成。 -
如果回拨时间很长,那么无法等待,可以匀出少量位(1~2 位)作为回拨位,一旦时钟回拨,将回拨位加 1,可得到不一样的 ID, 2 位回拨位允许标记 3 次时钟回拨,基本够使用。如果超出了,可以再选择抛出异常。
「雪花算法Java源代码」
/**
* 官方推出,Scala编程语言来实现的
* Java前辈用Java语言实现了雪花算法
*/
public class IdWorker{
//下面两个每个5位,加起来就是10位的工作机器id
private long workerId; //工作id
private long datacenterId; //数据id
//12位的序列号
private long sequence;
public IdWorker(long workerId, long datacenterId, long sequence){
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//初始时间戳
private long twepoch = 1288834974657L;
//长度为5位
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
//最大值
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列号id长度
private long sequenceBits = 12L;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//工作id需要左移的位数,12位
private long workerIdShift = sequenceBits;
//数据id需要左移位数 12+5=17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12+5+5=22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次时间戳,初始值为负数
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
//下一个ID生成算法
public synchronized long nextId() {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳
//说明:还处在同一毫秒内,则在序列号加1;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) { // 0 - 4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
/**
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取系统时间戳
private long timeGen(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
IdWorker worker = new IdWorker(21,10,0);
for (int i = 0; i < 100; i++) {
System.out.println(worker.nextId());
}
}
}
四、Redis的Incr命令获取全局唯⼀ID
「核心思想:Redis 的所有命令操作都是单线程的,本身提供像 incr 和 increby这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。」
「Redis Incr 命令将 key 中储存的数字值增⼀。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执⾏INCR 操作。」
-
「优点:」
-
不依赖于数据库,灵活方便,且性能优于数据库。 -
数字 ID 天然排序,对分页或者需要排序的结果很有帮助。 -
「缺点:」
-
如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度。 -
需要编码和配置的工作量比较大。 -
「优化方案:」考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量,
并利用上面的方案来配置集群
PS:比较适合使用 Redis 来生成每天从 0 开始的流水号。比如:“订单号=日期+当日自增长号”,则可以每天在 Redis 中生成一个 Key,使用 INCR 进行累加。
-
库水平拆分,设置不同的初始值和相同的步长;
-
批量缓存自增 ID
「案例:」

「Java代码中使⽤Jedis客户端调⽤Reids的incr命令获得⼀个全局的id」
1.引⼊jedis客户端jar
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.Java代码(此处我们就是连接单节点,也不使⽤连接池)
Jedis jedis = new Jedis("127.0.0.1",6379);
try {
long id = jedis.incr("id");
System.out.println("从redis中获取的分布式id为:" + id);
} finally {
if (null != jedis) {
jedis.close();
}
}
「Redis安装(安装单节点)」
-
官⽹下载redis-3.2.10.tar.gz
-
上传到linux服务器解压 tar -zxvf redis-3.2.10.tar.gz
tar -zxvf redis-3.2.10.tar.gz
-
cd 解压⽂件⽬录,对解压的redis进⾏编译
make
-
然后cd 进⼊src⽬录,执⾏make install
make install
-
修改解压⽬录中的配置⽂件redis.conf,关掉保护模式

-
在src⽬录下执⾏ ./redis-server ../redis.conf 启动redis服务
#../redis.conf 为redis.conf的目录
./redis-server ../redis.conf
原文始发于微信公众号(星河之码):分布式集群:分布式ID解决⽅案
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/26961.html