美团leaf学习笔记

1.0 简介
2.0 号段模式
3.0 总结

1.0 简介

对于分布式ID生成策略,之前我的印象里只有twitter开源的snowflake算法,该算法详情参见:snowflake算法思考。后来看了美团技术博客中有写到美团开源了自己的分布式ID服务,还是很好奇,于是把代码拉下来看了一下,这里做一个简单的源码解读,后面也可以给自己一个参考,源码地址下面在最下面。同时,如果在看这篇源码解读博客前,建议还是把美团官方Leaf博客先看一下。

2.0 号段模式

2.1 号段模式解释

号段模式其实就是每次取出一个区间号码,保存到内存中,等这批号码用完了后再从数据库中拿下一批。

2.2 号段模式Leaf如何用

Leaf号段模式实现原理就是在DB中建一张表,记录当前号段的起点和步长,当这个区间的号码都用完时,先更新数据库中起点值为当前使用号段的最大值,然后将这个值取出来作为新的号段的起点。

2.3 Leaf号段模式初始化

号段模式初始化参见SegmentService构造方法,具体代码如下:

public SegmentService() throws SQLException, InitException {
    // 将在leaf.properties文件
    Properties properties = PropertyFactory.getProperties();
    // 判断配置文件中是否设置为号段模式
    boolean flag = Boolean.parseBoolean(properties.getProperty(Constants.LEAF_SEGMENT_ENABLE, "true"));
    if (flag) {
        // Config dataSource
        dataSource = new DruidDataSource();
        dataSource.setUrl(properties.getProperty(Constants.LEAF_JDBC_URL));
        dataSource.setUsername(properties.getProperty(Constants.LEAF_JDBC_USERNAME));
        dataSource.setPassword(properties.getProperty(Constants.LEAF_JDBC_PASSWORD));
        dataSource.init();

        // 初始化Druid数据源,初始化mybatis SqlSessionFactory
        IDAllocDao dao = new IDAllocDaoImpl(dataSource);

        // 分布式ID生成核心类
        idGen = new SegmentIDGenImpl();
        // 依赖注入
        ((SegmentIDGenImpl) idGen).setDao(dao);
        if (idGen.init()) {
            logger.info("Segment Service Init Successfully");
        } else {
            throw new InitException("Segment Service Init Fail");
        }
    } else {
        // 测试类,只能输出0
        idGen = new ZeroIDGen();
        logger.info("Zero ID Gen Service Init Successfully");
    }
}

这里需要明确一个概念Tag,一个leaf服务是可以同时给很多业务同时提供服务的,所以A业务中会有id=1情况,B业务也有id=1的订单,Leaf只是保证同一业务中永远不会出现两个一样的id值。
SegmentIDDaoImpl类初始化代码如下:

@Override
public boolean init() {
    logger.info("Init ...");
    // 确保加载到kv后才初始化成功
    updateCacheFromDb();
    initOK = true;
    // 每分钟查一次DB,找到当前表中所有tag,移除已经不用的tag,发现新添加的tag
    updateCacheFromDbAtEveryMinute();
    return initOK;
}
2.4 Leaf如何取号

Leaf默认情况下使用双缓冲区策略取号,也就采用两个号段,一个主一个备,主号段使用完后备用buffer切换为主,主降级为次,同时更新次号段最大号码值。其实正常情况下一个buffer就好,当一个buffer区号段用完后,重新向DB中取下一个号段即可,但是根据美团官方博客中解释该种模式下会存在两个比较明显的问题:毛刺现象,就是在号被用完后需要到DB中取号,取号分两个阶段,更新最大号码为当前最大号码+步长,然后取更新后的最大号码值为当前buffer的最大号码值,但是如果DB update操作耗时较长,导致分布式ID服务在这个时候响应时间较长。DB在更新时宕机,update失败,服务在MySQL主从切换期间不可用。MySQL主从切换当然是一个相对来说比较快的过程,不过对于每秒海量请求的美团线上服务,这个时延也是不可接受的,于是在主buffer基础上增加一个备用buffer,保证MySQL挂掉的一段时间内,备用buffer可以继续起作用,保证分布式ID服务的高可用。

2.5 Leaf取号原理

这块主要是源码解读,Leaf双buffer核心类SegmentBuffer,这个类通过一个Segment数组以及一个currentPos变量来标示当前正在其作用的段。调用get方法时,Leaf首先从缓存中拿到当前的SegmentBuffer对象。
如果取出的SegmentBuffer未完成初始化,先进行SegmentBuffer初始化,初始化方式就是从DB中获取到当前号段最大ID值,赋值给当前SegmentBuffer对象。如果如果取出的SegmentBuffer已经初始化完成,则表示这个SegmentBuffer中号已经用完了,Leaf需要根据该步长范围号码消耗时间来判断步长设置的长短,如果步长设置过短,导致应用需频繁去DB中获取下一个最大值,影响应用性能。如果设置过大,可能号码很长时间都用不完,DB在下一次更新最大值号段值与前一次更新设置的步长值相差太大,我个人感觉这个好像也没啥问题吧。下面是官方在实际生产中碰到的问题解释:

号段长度始终是固定的,假如Leaf本来能在DB不可用的情况下,维持10分钟正常工作,那么如果流量增加10倍就只能维持1分钟正常工作了。
号段长度设置的过长,导致缓存中的号段迟迟消耗不完,进而导致更新DB的新号段与前一次下发的号段ID跨度过大。

SegmentBuffer中取号代码如下:

public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
    while (true) {
        buffer.rLock().lock();
        try {
			// 拿到当前正在使用的号段
            final Segment segment = buffer.getCurrent();
            // 如果下一个buffer还未准备好并且当前号段已经使用了10%,启动SegmentBuffer中线程执行备用buffer初始化
            if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
                service.execute(new Runnable() {
                    @Override
                    public 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的写锁,对buffer属性进行赋值
                                buffer.wLock().lock();
                                // 第一个号段用完后可以切换到第二个号段了
                                buffer.setNextReady(true);
                                buffer.getThreadRunning().set(false);
                                buffer.wLock().unlock();
                            } else {
                                buffer.getThreadRunning().set(false);
                            }
                        }
                    }
                });
            }
            // 从主buffer中获取到号码
            long value = segment.getValue().getAndIncrement();
            if (value < segment.getMax()) {
                return new Result(value, Status.SUCCESS);
            }
        } finally {
            buffer.rLock().unlock();
        }
        // 如果前面获取号码失败,则表示主buffer号码已经用完了,此时需要启用备用buffer进行号段分配。这里做一个类似于CAS的自旋操作,此时一个segmentBuffer已经用完了,下一个号段还未准备好,等下一个号段准备好后
        // 进行buffer切换,计数器才能正常工作
        waitAndSleep(buffer);
        buffer.wLock().lock();
        try {
            final Segment segment = buffer.getCurrent();
            long value = segment.getValue().getAndIncrement();
            if (value < segment.getMax()) {
                return new Result(value, Status.SUCCESS);
            }
            if (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();
        }
    }

主备切换过程Leaf的做法是调用SegmentBuffer#switchPos方法,具体做法如下:

public int nextPos() {
    return (currentPos + 1) % 2;
}

public void switchPos() {
    currentPos = nextPos();
}

可以看出就是一个偶数变奇数或者奇数变偶数过程,并没有什么复杂的骚操作。

3.0 总结

至此,Leaf号段模式就解释完了,这里还有基于snowflake算法实现或者zookeeper实现,但是个人感觉基于DB的实现跟可靠,从DB架构层次去做高可用可能比snowflake或zk实现跟成熟。同时,由于Leaf服务部署肯定是多节点,保证节点之间相对递增,如果采用snowflake还需要引入其它号段同步组建,更复杂吧。基于ZK这种ZAB协议实现的结构,可能在写性能上会有很大的障碍,如果多接点频繁进行ZK节点创建与删除,可能ZK会成为最后的瓶颈。

Leaf Github地址: https://github.com/Meituan-Dianping/Leaf

参考地址:
Leaf:美团分布式ID生成服务开源

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据提供的引用内容,我无法找到关于MyBatis Plus使用美团Leaf的具体信息。但是,我可以为您提供一些关于MyBatis Plus和Leaf的基本介绍。 MyBatis Plus是一个基于MyBatis的增强工具,它简化了MyBatis的开发流程,提供了更多的便利功能,如自动生成代码、分页查询、逻辑删除等。它可以与各种数据库进行集成,并提供了丰富的API和注解,使得开发更加高效和便捷。 美团Leaf是一个分布式ID生成系统,它可以生成全局唯一的ID。Leaf提供了两种ID生成算法:基于数据库的分段生成ID和雪花算法生成分布式ID。这些算法可以确保生成的ID在分布式环境下是唯一的。 如果您想在MyBatis Plus中使用美团Leaf,您可以按照以下步骤进行操作: 1. 首先,您需要将Leaf集成到您的项目中。您可以通过引用美团Leaf的Git仓库或者下载源代码来获取Leaf。 2. 然后,您需要根据Leaf的文档和示例代码,配置Leaf的相关参数,如数据库连接信息、ID生成算法等。 3. 接下来,您可以在MyBatis Plus的Mapper接口中定义一个方法,用于生成ID。您可以使用Leaf提供的API来生成ID,并将其作为参数传递给您的SQL语句。 4. 最后,您可以在您的业务逻辑中调用该方法,以生成唯一的ID。 请注意,以上步骤仅为一种可能的集成方式,具体的实现方式可能因项目需求和配置而有所不同。建议您参考美团Leaf的官方文档和示例代码,以获得更详细的集成指南和使用方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值