美团Leaf——全局序列生成器
Leaf的Github地址:
https://github.com/Meituan-Dianping/Leaf
Leaf美团技术团队博客地址:
https://tech.meituan.com/2017/04/21/mt-leaf.html
关于Leaf的使用手册、架构说明、Segment和Snowflake的特点和时钟回拨解决办法,参考上面的链接内容都能获得到答案。拒绝重复搬砖。
在本篇博客里我想就Leaf的Segment模式的源码实现做个简单的注释。代码分支:master。
1.Segment
Segment是SegmentBuffer的成员属性,cache中存储的是SegmentBuffer,Segment是双buffer的实现。
2.初始化Segment
在初始化Segment时,主要做两件事情。1是根据数据库表中配置的busi_tag更新缓存;2是添加定时任务,定时(一分钟间隔)更新缓存。
@Override
public boolean init() {
logger.info("Init ...");
// 确保加载到kv后才初始化成功
updateCacheFromDb();
initOK = true;
updateCacheFromDbAtEveryMinute();
return initOK;
}
updateCacheFromDb():
private void updateCacheFromDb() {
logger.info("update cache from db");
StopWatch sw = new Slf4JStopWatch();
try {
//从配置的数据源中加载biz_tag
List<String> dbTags = dao.getAllTags();
if (dbTags == null || dbTags.isEmpty()) {
return;
}
//cache中的biz_tag.初始为空.
List<String> cacheTags = new ArrayList<String>(cache.keySet());
//存储本次更新操作,要从DB中加载进cache的biz_tag.
Set<String> insertTagsSet = new HashSet<>(dbTags);
//存储失效的biz_tag:存在于cache,不存在于DB.
Set<String> removeTagsSet = new HashSet<>(cacheTags);
//过滤去重,得到需要存入进cache的biz_tag
for(int i = 0; i < cacheTags.size(); i++){
String tmp = cacheTags.get(i);
if(insertTagsSet.contains(tmp)){
insertTagsSet.remove(tmp);
}
}
//存储进cache
for (String tag : insertTagsSet) {
SegmentBuffer buffer = new SegmentBuffer();
buffer.setKey(tag);
Segment segment = buffer.getCurrent();
segment.setValue(new AtomicLong(0));
//这里的max、step均为0.所以这一步仅仅将biz_tag存储进了cache,并没有对SegmentBuffer执行初始化操作.
segment.setMax(0);
segment.setStep(0);
cache.put(tag, buffer);
logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
}
//cache中已失效的biz_tag从cache删除
for(int i = 0; i < dbTags.size(); i++){
String tmp = dbTags.get(i);
if(removeTagsSet.contains(tmp)){
removeTagsSet.remove(tmp);
}
}
for (String tag : removeTagsSet) {
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");
}
}
3.获取到下一个序列号
@Override
public Result get(final String key) {
if (!initOK) {
return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);
}
//判断cache中是否有这个key——bizTag
if (cache.containsKey(key)) {
SegmentBuffer buffer = cache.get(key);
//未初始化,
if (!buffer.isInitOk()) {
synchronized (buffer) {
//再次判断,以防重复初始化.
if (!buffer.isInitOk()) {
try {
//对SegmentBuffer当前的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);
}
}
}
}
return getIdFromSegmentBuffer(cache.get(key));
}
return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
}
updateSegmentFromDb();
public void updateSegmentFromDb(String key, Segment segment) {
StopWatch sw = new Slf4JStopWatch();
SegmentBuffer buffer = segment.getBuffer();
LeafAlloc leafAlloc;
//1.SegmentBuffer尚未初始化,则SegmentBuffer的step等于数据库中的step;
if (!buffer.isInitOk()) {
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
buffer.setStep(leafAlloc.getStep());
buffer.setMinStep(leafAlloc.getStep());//leafAlloc中的step为DB中的step
} else if (buffer.getUpdateTimestamp() == 0) {
//2.SegmentBuffer已经初始化完成:
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(leafAlloc.getStep());
buffer.setMinStep(leafAlloc.getStep());//leafAlloc中的step为DB中的step
} else {
//2.SegmentBuffer已经初始化完成:
//a) SegmentBuffer的存活时间小于15分钟:
// i. 如果SegmentBuffer当前的step*2大于最大值(一百万),则什么也不做,不再扩大step
// ii. 否则,SegmentBuffer的step扩大为原来的2倍。
//b) SegmentBuffer的存活时间小于30分钟:什么也不做.
//c) SegmentBuffer的存活时间在15至30分钟之间:
// i. 如果SegmentBuffer的step/2 大于等于数据库中的step,那么就将SegmentBuffer的step值变为原来的二分之一,否则什么也不做。
long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp();
int nextStep = buffer.getStep();
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;
}
logger.info("leafKey[{}], step[{}], duration[{}mins], nextStep[{}]", key, buffer.getStep(), String.format("%.2f",((double)duration / (1000 * 60))), nextStep);
LeafAlloc temp = new LeafAlloc();
temp.setKey(key);
temp.setStep(nextStep);
leafAlloc = dao.updateMaxIdByCustomStepAndGetLeafAlloc(temp);
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(nextStep);
buffer.setMinStep(leafAlloc.getStep());//leafAlloc的step为DB中的step
}
// must set value before set max
long value = leafAlloc.getMaxId() - buffer.getStep();
segment.getValue().set(value);
segment.setMax(leafAlloc.getMaxId());
segment.setStep(buffer.getStep());
sw.stop("updateSegmentFromDb", key + " " + segment);
}
getIdFromSegmentBuffer();
public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
while (true) {
//读锁
buffer.rLock().lock();
try {
//获取到当前正在使用的Segment.
final Segment segment = buffer.getCurrent();
//如果下一个Segment没有初始化完成,当前Segment的ID闲置数量小于9成,并且没有在执行Segment初始化操作,就去执行对下一个Segment的初始化.
//只要当前号段消费数量达到了10%,就对下一个号段进行初始化.
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 {
//根据biz_tag,根据DB对Segment初始化
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.setNextReady(true);
buffer.getThreadRunning().set(false);
buffer.wLock().unlock();
} else {
buffer.getThreadRunning().set(false);
}
}
}
});
}
//Segment的value记录了当前已分配的ID最大值,加一后返回.原子性操作.
long value = segment.getValue().getAndIncrement();
//如果超过了当前Segment缓存的最大值(当前Segment号段已经消费完毕),就进入阻塞等待.
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
} finally {
buffer.rLock().unlock();
}
//当前号段已经消费完了,下个号段正在有别的线程在执行初始化,进入等待.
//做空转.
waitAndSleep(buffer);
buffer.wLock().lock();
//下面这段代码在做空转之前已经执行过,这里为什么还要再次执行呢?
//在当前线程空转期间,可能已经有别的线程执行完毕了对下个Segment的初始化操作,并进行了切换.防止出现多次切换.
try {
final Segment segment = buffer.getCurrent();
long value = segment.getValue().getAndIncrement();
if (value < segment.getMax()) {
return new Result(value, Status.SUCCESS);
}
//当前Segment缓存的号段已经消费完了,下一个Segment初始化好了就进行切换
if (buffer.isNextReady()) {
//将Segment切换为下一个.
buffer.switchPos();
buffer.setNextReady(false);
} else {
//ERROR.当前Segment号段满了,下一个号段还未准备好.
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——全局序列生成器的更多相关文章
- 分布式全局ID生成器原理剖析及非常齐全开源方案应用示例
为何需要分布式ID生成器 **本人博客网站 **IT小神 www.itxiaoshen.com **拿我们系统常用Mysql数据库来说,在之前的单体架构基本是单库结构,每个业务表的ID一般从1增,通过 ...
- 分布式全局ID生成器设计
项目是分布式的架构,需要设计一款分布式全局ID,参照了多种方案,博主最后基于snowflake的算法设计了一款自用ID生成器.具有以下优势: 保证分布式场景下生成的ID是全局唯一的 生成的全局ID整体 ...
- Mycat配置分库分表(垂直分库、水平分表)、全局序列
1. Mycat相关文章 Linux安装Mycat1.6.7.4并实现Mysql数据库读写分离简单配置 Linux安装Mysql8.0.20并配置主从复制(一主一从,双主双从) Docke ...
- 分布式 ID 解决方案之美团 Leaf
分布式 ID 在庞大复杂的分布式系统中,通常需要对海量数据进行唯一标识,随着数据日渐增长,对数据分库分表以后需要有一个唯一 ID 来标识一条数据,而数据库的自增 ID 显然不能满足需求,此时就需要有一 ...
- mycat - 全局序列
解决主键冲突问题:例如id自增的order表,如果分布式情况下不处理的话,当每个表的第一条数据id都是1. 怎么确保id唯一呢? 解决办法: 1.本地文件(不推荐) 2.数据库方式(推荐) 3.时间戳 ...
- ALTER SEQUENCE - 更改一个序列生成器的定义
SYNOPSIS ALTER SEQUENCE name [ INCREMENT [ BY ] increment ] [ MINVALUE minvalue | NO MINVALUE ] [ MA ...
- MYCAT全局序列
1.本地文件方式 sequnceHandlerType = 0 /root/data/program/mycat/conf/server.xml <property name="s ...
- 分布式全局ID生成方案
传统的单体架构的时候,我们基本是单库然后业务单表的结构.每个业务表的ID一般我们都是从1增,通过AUTO_INCREMENT=1设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多 ...
- 常用的分布式ID生成器
为何需要分布式ID生成器 **本人博客网站 **IT小神 www.itxiaoshen.com **拿我们系统常用Mysql数据库来说,在之前的单体架构基本是单库结构,每个业务表的ID一般从1增,通过 ...
随机推荐
- web前端开发入门全套学习方法路径,兼职在家做网站也能月入上万!
前端学习路径 1.WEB前端快速入门 在本阶段,我们需要掌握 HTML 与 CSS 基础,当然,也包含 H5 和 C3 的新特性.这个部分内容非常简单,而且非常容易掌握.相信你也更愿意学习这个部分,毕 ...
- H5和原生的职责划分
前言 在JSBridge实现后,前端网页与原生的交互已经通了,接下来就要开始规划API,明确需要提供哪一些功能来供前端调用. 但是在这之前,还有一点重要工作需要做: 明确H5与Native的职责划分, ...
- js常用删除
1.对象删除 1.1删除属性 用(delete 对象.属性 )的方式进行: var people={"name":"zhangsan",age:18}; del ...
- day48 work
1 navicat自己玩一玩 2 练习题一定要搞懂 照着我的思路一遍遍的看敲 3 熟悉pymysql的使用 4 sql注入产生的原因和解决方法 了解 5 思考:如何结合mysql实现用户的注册和登录功 ...
- CSS3 实现复杂的背景图案
灵活的背景定位 实现效果: 将背景图定位到距离容器底边 10px 且距离右边 20px 的位置. background-position 方案 代码: <div>海盗密码</div& ...
- day33 网络编程(下)
目录 上节课回顾: 一.传输层 二.应用层 三.socket 四.如何获取目标ip地址 五.网络通信的流程 上节课回顾: 通过ip地址如何找到另外一台设备 ip地址分为子网部分和主机部分 我们要和其他 ...
- TP5中的缓存使用
Thinkphp 5.0采用了 think\Cache 类来提供缓存支持 缓存支持采用驱动方式,所以缓存在使用之前,需要进行连接操作,也就是缓存初始化操作. 支持的缓存类型包括file.memcach ...
- Spring @Value注解使用${}进行注入(转)
原文:http://my.oschina.net/js99st/blog/632104 spring3中新增的@value注解 http://bijian1013.iteye.com/blog/202 ...
- flask 源码专题(五):SqlAlchemy 中操作数据库时session和scoped_session的区别
1原生session: from sqlalchemy.orm import sessionmaker from sqlalchemy import create_engine from sqlalc ...
- CSS数据样式
CSS数据样式 表格 定制表格 我们除了可以使用<table>标签进行绘制表格,在css3中display也支持进行表格的样式绘制. 样式规则 说明 table 对应 table tabl ...