美团Leaf分布式ID生成算法深度解析与源码实现

美团Leaf分布式ID生成算法深度解析与源码实现

前言

在分布式系统中,全局唯一ID的生成是核心基础服务。美团点评(现美团)针对Snowflake算法在运维场景中的痛点,研发了Leaf分布式ID生成系统。本文将从设计原理、源码实现、优化策略等角度深入剖析Leaf算法。


一、分布式ID生成方案对比

常见方案对比

方案 优点 缺点
UUID 简单 无序、字符串存储效率低
数据库自增ID 简单可靠 性能瓶颈、扩展困难
Redis生成ID 性能较好 依赖Redis、数据持久化问题
Snowflake 趋势递增、高性能 时钟回拨问题、机器位限制

Leaf核心优势

  1. 高可用性(双号段缓存 + Zookeeper容错)
  2. 解决时钟回拨问题
  3. 支持HTTP/API多协议接入
  4. 可视化监控支持

二、Leaf算法核心原理

Leaf包含两种模式,可独立部署也可混合使用:

2.1 Leaf-Segment(号段模式)

美团Leaf分布式ID生成算法深度解析与源码实现_第1张图片

实现原理:

  1. 使用数据库代理(号段表)分配ID区间
  2. 双Buffer异步加载机制
  3. 号段耗尽时自动切换Buffer

采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

源码深度解析

public class SegmentIDGenImpl implements IDGen {
   
    private static final Logger logger = LoggerFactory.getLogger(SegmentIDGenImpl.class);
 
    /**
     * IDCache未初始化成功时的异常码
     */
    private static final long EXCEPTION_ID_IDCACHE_INIT_FALSE = -1;
    /**
     * key不存在时的异常码
     */
    private static final long EXCEPTION_ID_KEY_NOT_EXISTS = -2;
    /**
     * SegmentBuffer中的两个Segment均未从DB中装载时的异常码
     */
    private static final long EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL = -3;
    /**
     * 最大步长不超过100,0000
     */
    private static final int MAX_STEP = 1000000;
    /**
     * 一个Segment维持时间为15分钟
     */
    private static final long SEGMENT_DURATION = 15 * 60 * 1000L;
     
    // 最多5个线程,也就是最多5个任务同时执行(因为可能有多个tag,如果tag 只有1、2个,那么没必要5个线程);idle时间是60s;
    // SynchronousQueue意思是,只能有一个线程执行一个tag的任务,立即执行,执行完立即获取;其他的都只能阻塞式等待
    private ExecutorService service = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new UpdateThreadFactory());
    private volatile boolean initOK = false;
     
    // 注意它包含了所有的SegmentBuffer,k是业务类型,v是对应的
    // 全局缓存
    private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<String, SegmentBuffer>();
    private IDAllocDao dao;
 
    public static class UpdateThreadFactory implements ThreadFactory {
   
 
        private static int threadInitNumber = 0;
 
        private static synchronized int nextThreadNum() {
   
            return threadInitNumber++;
        }
 
        @Override
        public Thread newThread(Runnable r) {
   
            return new Thread(r, "Thread-Segment-Update-" + nextThreadNum());
        }
    }
 
    @Override
    public boolean init() {
   
        logger.info("Init ...");
        // 确保加载到kv后才初始化成功
        updateCacheFromDb();
        initOK = true;
        updateCacheFromDbAtEveryMinute();
        return initOK;
    }
 
    private void updateCacheFromDbAtEveryMinute() {
   // 顾名思义, 每分钟执行一次。执行什么? 执行 updateCacheFromDb方法
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
   
            @Override
            public Thread newThread(Runnable r) {
   
                Thread t = new Thread(r);
                t.setName("check-idCache-thread");
                t.setDaemon(true);
                return t;
            }
        });
        service.scheduleWithFixedDelay(new Runnable() {
   
            @Override
            public void run() {
   
                updateCacheFromDb();
            }
        }, 60, 60, TimeUnit.SECONDS);
    }
 
 
// 通过数据库 来更新cache缓存, 也就是Map cache。注意它包含了所有的SegmentBuffer
// 总结就是, 把db新增的tag, 初始化并添加到cache,把db删除的tag,从cache中删除;db中更新的呢?这里不管,后面由更新线程去维护到cache
// init的时候执行一次,后面每分钟执行一次
    private void updateCacheFromDb() {
   
        logger.info("update cache from db");
        StopWatch sw = new Slf4JStopWatch();
        try {
   
            List<String> dbTags = dao.getAllTags();// 可能有新加的tags, 这里仅仅加载 tag,不包括value
            if (dbTags == null || dbTags.isEmpty()) {
   
                return;
            }
            List<String> cacheTags = new ArrayList<String>(cache.keySet());
            Set<String> insertTagsSet = 

你可能感兴趣的:(java,分布式,算法,leaf,美团分布式ID生成算法)