上一篇文章我们讲了几种常见的分布式唯一ID生成方案,今天我们再来讲一下由美团开源的Leaf框架,这个框架集成了两种最适合生产环境使用的方式

第一种方式是:Leaf Segment
这种方式其实跟我们之前讲过的flickr高并发方案类似,都是使用数据库来生成分布式唯一ID,同时生成的唯一ID代表了一整个号段,保证了数据库的高并发使用,同时里面还进行了一个双Buffer的优化,这个我们在后面的源码分析里面详细讲

我先把Leaf中的号段格式展示出来,这一段在Leaf官网中也有

+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag     | varchar(128) | NO   | PRI |                   |                             |
| max_id      | bigint(20)   | NO   |     | 1                 |                             |
| step        | int(11)      | NO   |     | NULL              |                             |
| desc        | varchar(256) | YES  |     | NULL              |                             |
| update_time | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+

里面核心的字段是biz_tag(业务标记),不同的业务使用Leaf的时候会有不同的标志,防止各个业务使用分布式唯一ID的时候错乱,大家各用各的唯一ID,这样的话一张表就可以维护多个不同业务的唯一ID了
max_id(最大唯一ID),上面我们提了一下生成的唯一ID代表了一整个号段,所以这个字段就表示了最大的唯一ID是多少
step(步长),通过步长和最大ID,我们就能得到最小ID,同时我们就能得到一整个号段了

Leaf Segment的核心实现在com.sankuai.inf.leaf.segment.SegmentIDGenImpl类中,我们详细分析一下里面的方法

@Override
public boolean init() {logger.info("Init ...");// 确保加载到kv后才初始化成功,具体解析在下面updateCacheFromDb();initOK = true;// 通过方法名就可以理解,这里实际上通过一个定时的线程池每分钟去调用updateCacheFromDb方法updateCacheFromDbAtEveryMinute();return initOK;
}
private void updateCacheFromDb() {logger.info("update cache from db");StopWatch sw = new Slf4JStopWatch();try {// 从数据库表(上面文中提到的表)中获取所有的biz_tagList<String> dbTags = dao.getAllTags();if (dbTags == null || dbTags.isEmpty()) {return;}// 初次调用这个方法的时候,这个缓存的tags还是空的,否则缓存的就是上一次已经初始化过的业务tagList<String> cacheTags = new ArrayList<String>(cache.keySet());// 将库中查询出来的所有tag数据都放到一个set中Set<String> insertTagsSet = new HashSet<>(dbTags);// 缓存的tag也都放到一个set中Set<String> removeTagsSet = new HashSet<>(cacheTags);for(int i = 0; i < cacheTags.size(); i++){String tmp = cacheTags.get(i);// 如果cache中已经存在了这个tag,就不算是新增的tag了,将其剔除if(insertTagsSet.contains(tmp)){insertTagsSet.remove(tmp);}}// 新的业务tag会生成一个新的号段,然后被放入到缓存的tag中for (String tag : insertTagsSet) {SegmentBuffer buffer = new SegmentBuffer();buffer.setKey(tag);Segment segment = buffer.getCurrent();segment.setValue(new AtomicLong(0));segment.setMax(0);segment.setStep(0);cache.put(tag, buffer);logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);}// 这里我举个例子比较好理解,比如说我原来缓存的tag有A、B、C,再次调用的时候只剩下了A、Bfor(int i = 0; i < dbTags.size(); i++){String tmp = dbTags.get(i);if(removeTagsSet.contains(tmp)){// 这一步操作将set中的A和B删掉了removeTagsSet.remove(tmp);}}for (String tag : removeTagsSet) {// 从缓存tag中将之前没有删的C给剔除了,从结果上来说就是将已失效的tags从cache中移除cache.remove(tag);logger.info("Remove tag {} from IdCache", tag);}} catch (Exception e) {logger.warn("update cache from db exception", e);} finally {sw.stop("updateCacheFromDb");}
}

updateCacheFromDb方法总体上就是将数据库中存在的biz_tag数据都缓存到一个cache中

在初始化之后我们就能得到一个cache,这个cache是一个ConcurrentHashMap,key是biz_tag,value是我们的buffer,这里面包含了唯一ID的号段

当我们将数据初始化完成之后,想要获得分布式唯一ID的话,只要通过get方法从我们的tag中获取即可,接下来我们看看这个里面做了哪些工作

@Override
public Result get(final String key) {// 未初始化完成的话就报错if (!initOK) {return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);}if (cache.containsKey(key)) {SegmentBuffer buffer = cache.get(key);// Double Checkif (!buffer.isInitOk()) {synchronized (buffer) {if (!buffer.isInitOk()) {try {// 如果buffer中的数据未初始化完成,就从数据库中更新Segment(Segment这个数据结构其实就是我们的卡号段)updateSegmentFromDb(key, buffer.getCurrent());logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent());buffer.setInitOk(true);} catch (Exception e) {logger.warn("Init buffer {} exception", buffer.getCurrent(), e);}}}}// 从SegmentBuffer中取出我们要的唯一IDreturn getIdFromSegmentBuffer(cache.get(key));}return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
}

updateSegmentFromDb方法其实不是很重要,逻辑就是根据step步长将我们的卡号段maxId = maxId + step,设置到数据库中,给我们一个新的号段去分发唯一ID,其中有一段优化稍微值得讲一下,我直接贴代码,实现很简单

long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
int nextStep = buffer.getStep();
// SEGMENT_DURATION为15分钟
if (duration < SEGMENT_DURATION) {if (nextStep * 2 > MAX_STEP) {//do nothing} else {nextStep = nextStep * 2;}
} else if (duration < SEGMENT_DURATION * 2) {//do nothing with nextStep
} else {nextStep = nextStep / 2 >= buffer.getMinStep() ? nextStep / 2 : nextStep;
}

我再贴一段Leaf官网中对这一段优化的描述,你们感受一下是不是感觉挺厉害,实际实现起来又相当简单

Leaf动态调整Step
假设服务QPS为Q,号段长度为L,号段更新周期为T,那么Q * T = L。最开始L长度是固定的,导致随着Q的增长,T会越来越小。但是Leaf本质的需求是希望T是固定的。那么如果L可以和Q正相关的话,T就可以趋近一个定值了。所以Leaf每次更新号段的时候,根据上一次更新号段的周期T和号段长度step,来决定下一次的号段长度nextStep:
T < 15min,nextStep = step * 2
15min < T < 30min,nextStep = step
T > 30min,nextStep = step / 2
至此,满足了号段消耗稳定趋于某个时间区间的需求。当然,面对瞬时流量几十、几百倍的暴增,该种方案仍不能满足可以容忍数据库在一段时间不可用、系统仍能稳定运行的需求。因为本质上来讲,Leaf虽然在DB层做了些容错方案,但是号段方式的ID下发,最终还是需要强依赖DB。

接下来到核心方法了,如何从buffer中取出我们想要的唯一ID

public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {while (true) {// 加读锁,提高并发性能buffer.rLock().lock();try {// 获取当前使用的号段final Segment segment = buffer.getCurrent();// 这里是一个关键的优化点,里面有一个双buffer的机制// 下一个号段还不可切换(未准备完成) && 当前号段已经使用了10% && CAS操作将备用buffer准备的线程置为运行中if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {service.execute(new Runnable() {@Overridepublic void run() {// 获取下一段要使用的号段Segment next = buffer.getSegments()[buffer.nextPos()];boolean updateOk = false;try {// 更新数据库并将新的号段设置到备用的buffer中updateSegmentFromDb(buffer.getKey(), next);updateOk = true;logger.info("update segment {} from db {}", buffer.getKey(), next);} catch (Exception e) {logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e);} finally {if (updateOk) {buffer.wLock().lock();// 下一个号段准备完毕,同时将备用buffer准备的线程置为未运行buffer.setNextReady(true);buffer.getThreadRunning().set(false);buffer.wLock().unlock();} else {buffer.getThreadRunning().set(false);}}}});}long value = segment.getValue().getAndIncrement();// 如果当前获取的唯一ID是在号段中就可以直接返回结果了if (value < segment.getMax()) {return new Result(value, Status.SUCCESS);}} finally {buffer.rLock().unlock();}// 判断备用线程是否准备完毕了,没好就再等等waitAndSleep(buffer);buffer.wLock().lock();try {final Segment segment = buffer.getCurrent();long value = segment.getValue().getAndIncrement();// 再次取出当前buffer的唯一ID出来看看是不是在号段内,是就直接返回结果if (value < segment.getMax()) {return new Result(value, Status.SUCCESS);}// 如果不行的话,就切换为备用buffer,也就是切换为下一个号段,下次再来取唯一IDif (buffer.isNextReady()) {buffer.switchPos();buffer.setNextReady(false);} else {logger.error("Both two segments in {} are not ready!", buffer);return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION);}} finally {buffer.wLock().unlock();}}
}

上面的方法不仅实现了从号段中取出了唯一ID的逻辑,还有一个双buffer的缓存机制,只要一个buffer使用了10%,备用的buffer就会去准备了,一旦当前buffer用完就切换为下一个buffer继续提供服务,保证获取分布式唯一ID的时候,几乎不会有再次从数据库中取号段的真空期

Leaf使用的第二种方式是:Snowflake
接下来讲讲Leaf通过Snowflake算法的实现,具体的实现类在com.sankuai.inf.leaf.snowflake.SnowflakeIDGenImpl中,首先是类的构造方法

public SnowflakeIDGenImpl(String zkAddress, int port, long twepoch) {this.twepoch = twepoch;Preconditions.checkArgument(timeGen() > twepoch, "Snowflake not support twepoch gt currentTime");final String ip = Utils.getIp();SnowflakeZookeeperHolder holder = new SnowflakeZookeeperHolder(ip, String.valueOf(port), zkAddress);LOGGER.info("twepoch:{} ,ip:{} ,zkAddress:{} port:{}", twepoch, ip, zkAddress, port);// 核心方法在这里,就是ZK的初始化,在ZK记录一些文件和节点,这里不细讲// 这里也有一个优化,就是ZK中获取的workerId也会缓存一份到本机文件系统上,即使ZK失效了,也能保证重启后能正常运行,做到了对ZK的弱依赖boolean initFlag = holder.init();if (initFlag) {workerId = holder.getWorkerID();LOGGER.info("START SUCCESS USE ZK WORKERID-{}", workerId);} else {Preconditions.checkArgument(initFlag, "Snowflake Id Gen is not init ok");}Preconditions.checkArgument(workerId >= 0 && workerId <= maxWorkerId, "workerID must gte 0 and lte 1023");
}

获取唯一ID的核心方法

@Override
public synchronized Result get(String key) {long timestamp = timeGen();// 如果产生了时钟回拨的情况if (timestamp < lastTimestamp) {long offset = lastTimestamp - timestamp;// 判断回拨时间小于5msif (offset <= 5) {try {// 等待10mswait(offset << 1);timestamp = timeGen();// 等待后时钟还未拨正的话就报错if (timestamp < lastTimestamp) {return new Result(-1, Status.EXCEPTION);}} catch (InterruptedException e) {LOGGER.error("wait interrupted");return new Result(-2, Status.EXCEPTION);}} else {// 时钟回拨时间过长的话直接报错返回return new Result(-3, Status.EXCEPTION);}}// 如果是同一时间生成的,则进行毫秒内序列if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {//seq 为0的时候表示是下一毫秒时间开始对seq做随机sequence = RANDOM.nextInt(100);timestamp = tilNextMillis(lastTimestamp);}} else {//如果是新的ms开始sequence = RANDOM.nextInt(100);}lastTimestamp = timestamp;// 经典snowflake实现long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;return new Result(id, Status.SUCCESS);
}

上面的方法中,SnowFlake算法的实现没什么好说的,还是老一套,但是Leaf针对时钟回拨做了一定的优化,当时钟回拨时间较短的话,就尝试进行等待,如果等了一段时间,时间已经回到回拨时间之后了就继续提供服务,否则就直接报错,回拨时间过长的话也是一样,直接报错,不再提供唯一ID的生成,防止唯一ID重复

总结下来,Leaf提供了两种可用性最高的生产方案,并且都进行了一定的优化,既能保证高可用,也能保证高并发,是一个非常成熟且易用的分布式唯一ID生成服务

Leaf(美团分布式ID生成服务)核心代码分析相关推荐

  1. Leaf:美团分布式ID生成服务开源

    Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家.数学家莱布尼茨的一句话:"There are no two identical leaves in the wor ...

  2. Leaf:美团分布式ID生成服务开源 1

    Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家.数学家莱布尼茨的一句话:"There are no two identical leaves in the wor ...

  3. 美团分布式ID生成服务开源!

    上一篇:突然!VS Code 杀死 IDEA?! 简介 Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也 ...

  4. 美团分布式ID生成服务LeafCode

    分布式ID作用 复杂系统中往往需要唯一标识做一些事情,比如全局唯一code码,比如分库分表用系统递增id不能满足需求,比如做IM消息id需要有个生成器. 业务上对于全局唯一ID有什么要求呢? 全局唯一 ...

  5. Leaf-美团分布式ID生成服务

    Leaf : 美团分布式ID生成服务 There are no two identical leaves in the world.(世界上没有两片相同的树叶.) - 莱布尼茨 现有分布式ID生成方案 ...

  6. 深度解析leaf分布式id生成服务源码(号段模式)

    原创不易,转载请注明出处 文章目录 前言 1.实现原理推演 1.1 基于mysql最简单分布式ID实现 1.2 flickr分布式id解决方案 1.3 号段+mysql 2.源码剖析 2.1初始化 2 ...

  7. 美团开源分布式ID生成系统——Leaf源码阅读笔记(Leaf的号段模式)

    Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也有的业务直接用UUID这种方式来生成ID.以上的方式各自 ...

  8. 美团分布式mysql_9种分布式ID生成之美团(Leaf)实战

    整理了一些Java方面的架构.面试资料(微服务.集群.分布式.中间件等),有须要的小伙伴能够关注公众号[程序员内点事],无套路自行领取javascript 更多优选java 引言 前几天写过一篇< ...

  9. 美团(Leaf)分布式ID生成器,好用的一批!

    不了解分布式ID的同学,先行去看<一口气说出 9种 分布式ID生成方式,面试官有点懵了>温习一下基础知识,这里就不再赘述了 美团(Leaf) Leaf是美团推出的一个分布式ID生成服务,名 ...

最新文章

  1. 【Live555】live555源码详解系列笔记
  2. getchar()到底怎么用_脱霉剂到底该怎么用?
  3. SAP UI 搜索分页技术
  4. fiddler修改支付金额_不容忽视的记账工具:支付宝记账
  5. 3-1:HTTP协议之应用层协议了解
  6. DTC精彩回顾—金学东:从可迁到好迁:人大金仓打造国产数据库生态 助力企业国产化转型...
  7. php服务器错误日志在哪里看,PHP取服务器错误日志
  8. android listview 分页
  9. centos7 php多版本切换_CentOS7服务搭建----搭建私有云盘01
  10. java堆栈_java线程的堆栈跟踪之jstack篇
  11. read()/write()的生命旅程之四——第四章:writeback
  12. JavaWbe中文乱码解决方案
  13. Vue 使用Echarts
  14. UDP的单播广播和组播
  15. 视频动作识别(Action Recognition)综述
  16. lerna + yarn workspaces 使用备忘
  17. 结构化数据和非结构化数据的分析
  18. composer global require fxp/composer-asset-plugin:1.0.0的Not enough arguments解决
  19. 1024程序员狂欢节,来领当当大额优惠券
  20. 基于Spark实现电影点评系统用户行为分析—RDD篇(一)

热门文章

  1. 为什么php面试笔试题,PHP笔试题
  2. 访问ChatGPT(openai)出现Access denied(拒绝访问)或则429 You are being rate limited.(429 您受到速率限制)
  3. 《父亲家书》选:父母的生日
  4. discuz!X2头像无法显示解决方法
  5. 中国科大自主研发的机器人柔性手爪获RoboCup“最佳操作奖
  6. 什么是GIoU Loss?
  7. K12教育培训机构未来的发展趋势
  8. 算法导论 — 思考题15-4 整齐打印
  9. echarts图表legend整齐排列
  10. vulhub靶场-weblogic漏洞复现