前言
昨天沉思君分享了一篇关于分布式ID生成方案的文章《分布式ID常见解决方案》,文中介绍了几种常见的分布式ID生成方案,并讨论了其优缺点。刚好最近沉思君在看李艳鹏老师合著的书《可伸缩服务架构:框架与中间件》,里面也有谈到分布式ID的设计思路,该项目已在GitHub上开源,项目名称为Vesta,今天就简单给大家介绍下Vesta的设计思路。
数据结构
首先介绍下Vesta中ID的数据结构。Vesta的ID类型是Java的long型,也就是二进制的64位。其中根据不同的业务场景,Vesta提供了2种ID类型给使用者,分别是最大峰值型和最小粒度型。每种类型的不同比特位代表的含义略有不同。
字段 | 版本 | 类型 | 生成方式 | 秒级时间 | 序列号 | 机器ID |
---|---|---|---|---|---|---|
位数 | 63 | 62 | 60-61 | 30-59 | 10-29 | 0-9 |
因为使用的是秒级时间,所以时间段的位数比较少,而序列号位数相对比较多,着意味着1秒内可以承受的峰值比较高。
字段 | 版本 | 类型 | 生成方式 | 毫秒级时间 | 序列号 | 机器ID |
---|---|---|---|---|---|---|
位数 | 63 | 62 | 60-61 | 20-59 | 10-19 | 0-9 |
由于时间精确到毫秒级,故粒度比较小,单位毫秒内生成的序列号比较少,适合用于峰值不高的场景。
其中,版本的默认值是0,其作用是用于扩展位或者扩容时的临时方案,比如当ID快用完的时候,只要将版本号修改为1,又可以使用很多年了。
生成类型表示的是ID生成器的发布模式,Vesta目前支持3种发布模式,分别是:嵌入发布模式,中心服务器发布模式和REST发布模式,一共3种,所以用2个二进制位就可以表示了。嵌入发布模式指的是直接调用本地Jar的方式生成分布式ID,这种方式需要提前配置好本地机器ID,优点是不需要依赖中心服务器。中心服务器发布模式指的是通过远程过程调用的方式调用中心服务器来生成分布式ID,缺点是需要依赖于中心服务器。REST发布模式指的是通过REST API调用中心服务器来生成分布式ID,优点是支持多语言,而前2种方式目前只支持Java客户端。
机器ID指的是生成分布式ID的机器对应的ID,Vesta支持3种机器ID分配方式,分别是属性配置方式、IP映射方式和数据库配置方式。下面将对这几种机器ID配置方式进行说明。
机器ID配置方式
属性配置方式,顾名思义就是在配置文件或机器的环境变量中配置好机器ID。这种方式适用于机器数量少的情况,如果机器数量多,配置起来是很繁琐的。
IP映射方式,则是将所有需要生成ID的机器IP分别映射成唯一的机器ID,并配置在配置文件里,当服务启动的时候,将IP跟机器ID的映射初始化到Map中,再根据所在机器的IP就可以得到该机器对应的ID了。这种方式配置起来比较简单。
数据库配置方式则是将机器IP跟机器ID的映射配置在数据库里,这种方式配置起来也比较简单,缺点是需要依赖外部数据库。
序列号生成方式
接下来讲下Vesta中的序列号生成方式。由于在同一台机器上可能存在多线程并发申请生成ID,所以生成单位时间内的序列号时需要考虑同步。Vesta中有3种生成序列号的方式,分别是基于Synchronized锁方式、基于ReentranLock方式和基于CAS无锁技术方式。
其中,基于Synchronized锁方式和基于基于ReentranLock方式比较简单,就是在递增序列号时加锁来进行同步,在此不赘述。
基于CAS无锁技术的方式使用了原子变量引用实现,因为生成ID时,需要同时更新时间戳和序号,为了实现原子更新,故使用了原子变量引用AtomicReference。该类提供了compareAndSet方法来实现CAS操作。如果CAS操作失败,则进入下一轮循坏,继续尝试更新,直到更新成功为止。
核心代码如下:
public class AtomicIdPopulator implements IdPopulator, ResetPopulator {
class Variant {
private long sequence = 0;
private long lastTimestamp = -1;
}
private AtomicReference<Variant> variant = new AtomicReference<Variant>(new Variant());
public AtomicIdPopulator() {
super();
}
public void populateId(Id id, IdMeta idMeta) {
Variant varOld, varNew;
long timestamp, sequence;
while (true) {
// Save the old variant
varOld = variant.get();
// populate the current variant
timestamp = TimeUtils.genTime(IdType.parse(id.getType()));
TimeUtils.validateTimestamp(varOld.lastTimestamp, timestamp);
sequence = varOld.sequence;
if (timestamp == varOld.lastTimestamp) {
sequence++;
sequence &= idMeta.getSeqBitsMask();
if (sequence == 0) {
timestamp = TimeUtils.tillNextTimeUnit(varOld.lastTimestamp, IdType.parse(id.getType()));
}
} else {
sequence = 0;
}
// Assign the current variant by the atomic tools
varNew = new Variant();
varNew.sequence = sequence;
varNew.lastTimestamp = timestamp;
if (variant.compareAndSet(varOld, varNew)) {
id.setSeq(sequence);
id.setTime(timestamp);
break;
}
}
}
public void reset() {
variant = new AtomicReference<Variant>(new Variant());
}
更多细节可以下载项目源码进行了解。项目地址:
https://github.com/cloudatee/vesta-id-generator