互联网大厂的分布式ID解决方案(中)
上一篇分析了百度的UidGenerator源码,这篇将继续分析滴滴的TinyId的源码。对于TinyId的使用和介绍请参考官网,本篇文章侧重点在于对源码的分析。
01
—
号段算法
号段算法指的是预先生成一段数字号码,然后调用者在调用的时候,直接从预先生成好的号段中获取id,这个思想和百度UidGenerator中第二种策略相似。滴滴的TinyId是基于MySQL的自增长主键来预生成号段。
上图中0到1000是一个号段,如果这些号段在MySQL中生成的话会影响性能,占用资源,MySQL中可以只存储end_index,然后通过步长来计算下一个号段,例如:end_id=1000,step=1000,下一个号段是从end_id = 1000开始,结束是end_id + step = 2000。然后JVM拿到这两个字段之后就开始加载这个号段的数据,这个就是号段的大概原理。TinyId在这个基础上面又做了一些优化,上篇文章提到百度的UidGenerator是使用双循环数组,然后采用了启动填充、定时填充、超过50%的阈值填充,TinyId也是类似的思路,做了两个号段,也就是双号段,就是提前再生成一个号段,这样就会尽可能的保证生产者速度高于消费者,总结一下滴滴的TinyId。
-
全局唯一的long型id
-
提供http和javaclient方式接入
-
支持批量获取id
-
支持生成1,3,5,7,9…序列的id
-
支持多个db的配置,无单点
-
适用场景:只关心id是数字,趋势递增的系统,可以容忍id不连续,有浪费的场景不适用场景:类似订单id的业务(因为生成的id大部分是连续的,容易被扫库、或者测算出订单量)
02
—
源码分析
【滴滴TinyId源码分析图-tinyid-server】
上图是TinyId-Server模块的获取id的核心的流程,对照着代码就可以梳理出来,再来看下滴滴的接口设计思路。
看下关系图,生成器接口(用来生成id的)、生成器工厂接口(用来生成生成器的包括客户端和服务端),核心的地方就是生成Id接口,看下id具体是怎么生成?来看下生成器的几个属性
/**
* 业务类型
*/
protected String bizType;
/**
* 号段业务接口:操作号段的接口
*/
protected SegmentIdService segmentIdService;
/**
* 当前号段
*/
protected volatile SegmentId current;
/**
* 下一个号段
*/
protected volatile SegmentId next;
/**
* 是否加载下一个号段
*/
private volatile boolean isLoadingNext;
/**
* 对象锁
*/
private Object lock = new Object();
/**
* 只有一个线程的线程池
*/
private ExecutorService executorService = Executors.newSingleThreadExecutor(new NamedThreadFactory("tinyid-generator"));
再看下号段信息的属性
/**
* 当前号段最大值
*/
private long maxId;
/**
* 预加载id:当前号段的20%,如果达到20%则加载下一个号段
*/
private long loadingId;
/**
* 当前id
*/
private AtomicLong currentId;
/**
* 每次增长的步长
*/
private int delta;
/**
* 余数
*/
private int remainder;
/**
* 是否初始化完成
*/
private volatile boolean isInit;
滴滴丰富了号段的功能,增加步长delta字段,步长是什么意思?数据库自增主键默认是按照+1来自增的,步长就是1,也可以设置步长为2,那每次自增长的时候就按照2来增长。为什么这样设定呢?可以增加数据库的数量,提高性能,例如:数据库A和数据库B同时支持号段功能,A从1开始,B从2开始,步长为2,那么A的递增情况是:1、3、5、7、9......,B的递增情况是:2、4、6、8、10......,可以保证A和B同时支持服务,但id不会重复。
号段生成器维护两个号段信息,一个是当前号段,一个是下一个号段。再看下构造器,也就是加载当前号段信息。public CachedIdGenerator(String bizType, SegmentIdService segmentIdService) {
this.bizType = bizType;
this.segmentIdService = segmentIdService;
//加载当前号段
loadCurrent();
}
public synchronized void loadCurrent() {
if (current == null || !current.useful()) {
if (next == null) {
//获取当前号段
SegmentId segmentId = querySegmentId();
this.current = segmentId;
} else {
current = next;
next = null;
}
}
}
再看下生成器怎么提供id给到外部调用的。
@Override
public Long nextId() {
while (true) {
if (current == null) {
//如果没有加载,则加载当前号段
loadCurrent();
continue;
}
Result result = current.nextId();
//如果id超过最大值,则从下一个号段开始,将下一个号段信息设置为当前号段
if (result.getCode() == ResultCode.OVER) {
loadCurrent();
} else {
//如果id超过loadingId,也就是预加载比例,则加载下一个号段信息
if (result.getCode() == ResultCode.LOADING) {
loadNext();
}
//获取id
return result.getId();
}
}
}
//加载下一个号段
public void loadNext() {
if (next == null && !isLoadingNext) {
synchronized (lock) {
if (next == null && !isLoadingNext) {
isLoadingNext = true;
executorService.submit(new Runnable() {
//后台默默执行下一个号段的加载
@Override
public void run() {
SegmentId segmentId = querySegmentId();
next = segmentId;
isLoadingNext = false;
}
});
}
}
}
}
总结一下生成器的核心功能:
1、加载当前号段并设置加载下一个号段的阈值为20%。
2、当前号段已经用了20%,则后台默默预加载下一个号段信息。
3、获取id超过当前号段的最大值,则将下一个号段设置成当前号段。
接下来再看下生成器的抽象工厂,为什么要一个抽象工厂?主要同时提供server端和client端两个模块使用。抽象工厂的作用就是根据业务生成一个生成器对象。每个客户端使用会分配一个业务类型bizType,根据业务类型区分。Server端就是上面提到的生成器,接下来看下客户端的生成器是怎么提供的。先来看下客户端生成器的核心属性。private static final String DEFAULT_PROP = "tinyid_client.properties";
private static final int DEFAULT_TIME_OUT = 5000;
private static String serverUrl = "http://{0}/tinyid/id/nextSegmentIdSimple?token={1}&bizType=";
在看初始化方法
private static void init(String location) {
idGeneratorFactoryClient = new IdGeneratorFactoryClient();
Properties properties = PropertiesLoader.loadProperties(location);
String tinyIdToken = properties.getProperty("tinyid.token");
String tinyIdServer = properties.getProperty("tinyid.server");
String readTimeout = properties.getProperty("tinyid.readTimeout");
String connectTimeout = properties.getProperty("tinyid.connectTimeout");
if (tinyIdToken == null || "".equals(tinyIdToken.trim())
|| tinyIdServer == null || "".equals(tinyIdServer.trim())) {
throw new IllegalArgumentException("cannot find tinyid.token and tinyid.server config in:" + location);
}
TinyIdClientConfig tinyIdClientConfig = TinyIdClientConfig.getInstance();
tinyIdClientConfig.setTinyIdServer(tinyIdServer);
tinyIdClientConfig.setTinyIdToken(tinyIdToken);
tinyIdClientConfig.setReadTimeout(TinyIdNumberUtils.toInt(readTimeout, DEFAULT_TIME_OUT));
tinyIdClientConfig.setConnectTimeout(TinyIdNumberUtils.toInt(connectTimeout, DEFAULT_TIME_OUT));
String[] tinyIdServers = tinyIdServer.split(",");
List serverList = new ArrayList(tinyIdServers.length);
for (String server : tinyIdServers) {
String url = MessageFormat.format(serverUrl, server, tinyIdToken);
serverList.add(url);
}
logger.info("init tinyId client success url info:" + serverList);
tinyIdClientConfig.setServerList(serverList);
}
看下参数serverUrl
"http://{0}/tinyid/id/nextSegmentIdSimple?token={1}&bizType=";
是一个http的请求,然后地址是server端获取号段的接口地址,根据业务类型和token。所以这个时候我们可以了解到,客户端其实就是通过服务端提供的号段接口获取号段信息。用意是将服务端的号段数据通过http的请求加载到客户端中,这样的作用可想而知,再一次提高了系统的性能,将压力分散到调用方,而且还能保证server端宕机,客户端还能够使用一段时间,优势相当明显。