> 文档中心 > 互联网大厂的分布式ID解决方案(中)

互联网大厂的分布式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给到外部调用的。​​​​​​​

@Overridepublic 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端宕机,客户端还能够使用一段时间,优势相当明显。

全民K歌电脑版