关于美团leaf的相关知识请到官方查看文档,有了一定了解之后,再根据本文实操。
美团leaf实现方式分为两种:Leaf-segment数据库方案(号段模式)、Leaf-snowflake方案(雪花算法模式)
一、Leaf-segment数据库方案
源码目录:
配置以下内容到:leaf-server------》resources------》leaf.properties
leaf.name=leaf服务名
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf?autoReconnect=true&useUnicode=true&useSSL=false&haracterEncoding=utf-8&&zeroDateTimeBehavior=convertToNull&&serverTimezone=GMT%2B8
leaf.jdbc.username=root
leaf.jdbc.password=你的数据库密码
接下来创建数据库、建表、插入数据:
CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 10, 'Test leaf Segment Mode Get Id')
为了演示效果明显,步长设置为10 ,实际生产时根据需要设定但最大步长不超过1000000
配置好以上信息后运行出现如下错误:
到leaf-parent下的pom文件中修改mysql-connector的版本即可:
运行成功后浏览器请求:localhost:8080/api/segment/get/mykey得到如下结果:
这里要注意localhost:8080/api/segment/get/{key},这里的key必须要与你数据库中保存的biz_tag下的值一致,否则请求失败。下面我们来一次正确的请求:localhost:8080/api/segment/get/leaf-segment-test 可以看到请求成功!
美团leaf提供了监控页面:http://localhost:8080/cache
其中init=true表示第一个segmentbuffer已准备好,next=false表示下一个segmentbuffer还没准备好,pos=0表示当前在segments[0],max0=11表示当前最大id为11,max0=max_id(数据库的值)+step=11。value0=max_id+1所以这里你会看到value0=2之后每次加1,当用完最后一个id的时候value0=value1+1(这里看不懂的读者可以不必理会,不影响请求结果,请求结果还是递增的)。这里还需要注意一个点当号段使用10%时会初始化下一个segmentbuffer。如图所示:
这里并不是达到10%时立马就更新进来,当达到10%时才fork一个新的进程去初始化下一个segment。所以你会看到当请求到id=3即value0=4时才更新下一个buffer如图所示。
当我用完第二个segment后,继续初始化第一个segment的时候发现步长竟然变成20了?
按道理来说步长是固定的,怎么翻倍增长了?我们来查看一下源码并解析:(想要快的读者直接跳过源码解析看结论)
public void updateSegmentFromDb(String key, Segment segment) {
StopWatch sw = new Slf4JStopWatch();
SegmentBuffer buffer = segment.getBuffer();
LeafAlloc leafAlloc;
if (!buffer.isInitOk()) {//判断是否初始化
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
buffer.setStep(leafAlloc.getStep());
//leafAlloc中的step为DB中的step
buffer.setMinStep(leafAlloc.getStep());
} else if (buffer.getUpdateTimestamp() == 0) {//判断当前时间戳是否为0
leafAlloc = dao.updateMaxIdAndGetLeafAlloc(key);
//若时间时间戳为0将当前时间戳作为Timestamp的值
buffer.setUpdateTimestamp(System.currentTimeMillis());
buffer.setStep(leafAlloc.getStep());
//leafAlloc中的step为DB中的step
buffer.setMinStep(leafAlloc.getStep());
} else {
//若当前时间戳不为0则用当前时间戳减去最近一次更新的时间戳。得到duration的值。
long duration = System.currentTimeMillis() -
buffer.getUpdateTimestamp();
int nextStep = buffer.getStep();
if (duration < SEGMENT_DURATION) {//SEGMENT_DURATION=15分钟
//当用当前时间戳减去最近一次更新的时间戳的差值小于15分钟时
//即duration<15*60*1000
//nextStep扩大一倍但不能超过最大步长1000000。MAX_STEP=1000000
if (nextStep * 2 > MAX_STEP) {
//当超过nextStep的两倍大于1000000时不做扩充。即按照原来的步长增长。
//do nothing
} else {
//duration小于15分钟,即一个segment的id15分钟内使用完了,
//且nextSrep*2<1000000时则扩大两倍
nextStep = nextStep * 2;
}
} else if (duration < SEGMENT_DURATION * 2) {
//如果15分钟<duration<30分钟时则按照原来的步长增长。
//do nothing with nextStep
} else {
//duration大于30分钟时则认为segment太大需要压缩到原来的1/2。
//但不能小于当期数据库存储的最大步长。
//getMinStep()获取的当前数据库的最大步长。
//若压缩一半小于当前数据库存储的最大step。
//则按照原来的步长增长,不做压缩。
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);
}
结论:一个segment 使用时间是15分钟-30分钟如果在15分钟就用完了一个segment的id则认为segment的步长太小会对其进行扩充,扩大为原来的两倍但上限不会超出1000000,如果步长的两倍超过1000000则按照原来的步长。如果超出30分钟才更新segment,则认为segment的步长太大,需要对其进行压缩,压缩为原来的1/2。但如果压缩1/2后比当前数库储存的最大步长小则不进行压缩。按照原来的步长增长。
总结:
Leaf-Segment模式有以下优缺点:
优点:
- Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
- ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
- 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
- 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。
缺点:
- ID号码不够随机,能够泄露发号数量的信息,不太安全。
- TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
- DB宕机会造成整个系统不可用。
二、Leaf-snowflake方案(雪花算法模式)
首先在leaf-server---》resources----》leaf.properties开启雪花模式并关闭Leaf-Segment模式。
两种模式不能同时开启。只能开启其中一种模式。
leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1 //zk服务器地址
leaf.snowflake.port=2181 //zk端口号。默认2181
启动leaf前需要先启动zookeeper,注意如果你是第一次下载zookeeper,同时也是第一次实践leaf,要注意端口号问题。zookeeper和Leaf会有端口占用问题,把zk或者leaf的其中一个端口号改掉即可。启动zk成功后启动Leaf。请求:localhost:8080/api/snowflake/get/key(这里的key可以随便填)可以看到请求成功!!!
请求结果是一个19位长度的long型数字。为什么是19位?2的10次方约等于10的3次方。雪花算法共64位。第1位符号位。随后41位时间戳。再后5位datacenterid,接着5位workid。后面12位为序号2的12次方等于4096即一毫秒能产生4096个不同的id即0-4095。一秒钟能产生4096000个不同id。2的64次方时间戳=69年。即能保证69年内不会出现id重复的情况。前提是不要发生时钟回拨或闰秒的情况,为了防止此类情况发生可以关闭NTP协议,不校对时间。
Leaf-snowflake方案有以下优缺点:
优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
缺点:
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
什么是时间回拨?
时钟回拨是硬件时钟可能会因为各种原因发生不准的情况,网络中提供了ntp服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题。即时间倒流或者时间增加。
什么是闰秒?闰秒
面试: