不能错过的分布式 ID 生成器(Leaf-),积累总结
max_id
:当前业务号段的最大值,用于计算下一个号段step
:步长,也就是每次获取 ID 的数量description
:对于业务的描述,没啥好说的
将 Leaf 项目下载到本地:https://github.com/Meituan-Dianping/Leaf
修改一下项目中的leaf.properties
文件,添加数据库配置
leaf.name=com.sankuai.leaf.opensource.testleaf.segment.enable=trueleaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/xin-master?useUnicode=tru
e&characterEncoding=utf8leaf.jdbc.username=junkangleaf.jdbc.password=junkangleaf.snowflake.enable=false
注意:leaf.snowflake.enable
与 leaf.segment.enable
是无法同时开启的,否则项目将无法启动。
配置相当的简单,直接启动LeafServerApplication
后就 OK 了,接下来测试一下,leaf
是基于Http请求
的发号服务, LeafController
中只有两个方法,一个号段接口,一个 snowflake 接口,key
就是数据库中预先插入的业务biz_tag
。
@RestControllerpublic?class?LeafController?{private?Logger?logger?=?LoggerFactory.getLogger(LeafController.class);
@Autowiredprivate?SegmentService?segmentService;@Autowiredprivate?SnowflakeService?snowflakeService;
/***?号段模式*?@param?key*?@return*/@RequestMapping(value?=?"/api/segment/get/{key}")public?String?getSegmentId(@PathVariable("key")?String?key)?{return?get(key,?segmentService.getId(key));}
/***?雪花算法模式*?@param?key*?@return*/@RequestMapping(value?=?"/api/snowflake/get/{key}")public?String?getSnowflakeId(@PathVariable("key")?String?key)?{return?get(key,?snowflakeService.getId(key));}
private?String?get(@PathVariable("key")?String?key,?Result?id)?{Result?result;if?(key?==?null?||?key.isEmpty())?{throw?new?NoKeyException();}result?=?id;if?(result.getStatus().equals(Status.EXCEPTION))?{throw?new?LeafServerException(result.toString());}return?String.valueOf(result.getId());}}
访问:http://127.0.0.1:8080/api/segment/get/leaf-segment-test
,结果正常返回,感觉没毛病,但当查了一下数据库表中数据时发现了一个问题。
通常在用号段模式的时候,取号段的时机是在前一个号段消耗完的时候进行的,可刚刚才取了一个 ID,数据库中却已经更新了max_id
,也就是说leaf
已经多获取了一个号段,这是什么鬼操作?
Leaf
为啥要这么设计呢?
Leaf
希望能在 DB 中取号段的过程中做到无阻塞!
当号段耗尽时再去 DB 中取下一个号段,如果此时网络发生抖动,或者 DB 发生慢查询,业务系统拿不到号段,就会导致整个系统的响应时间变慢,对流量巨大的业务,这是不可容忍的。
所以Leaf
在当前号段消费到某个点时,就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做很大程度上的降低了系统的风险。
那么某个点
到底是什么时候呢?
这里做了一个实验,号段设置长度为step=10
,max_id=1
当我拿第一个 ID 时,看到号段增加了,1/10
当我拿第三个 Id 时,看到号段又增加了,3/10
Leaf
采用双buffer
的方式,它的服务内部有两个号段缓存区segment
。当前号段已消耗 10%时,还没能拿到下一个号段,则会另启一个更新线程去更新下一个号段。
简而言之就是Leaf
保证了总是会多缓存两个号段,即便哪一时刻数据库挂了,也会保证发号服务可以正常工作一段时间。
通常推荐号段(segment
)长度设置为服务高峰期发号 QPS 的 600 倍(10 分钟),这样即使 DB 宕机,Leaf 仍能持续发号 10-20 分钟不受影响。
优点:
Leaf 服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
容灾性高:Leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 Leaf 仍能正常对外提供服务。
缺点:
ID 号码不够随机,能够泄露发号数量的信息,不太安全。
DB 宕机会造成整个系统不可用(用到数据库的都有可能)。
二、Leaf-snowflake
Leaf-snowflake
基本上就是沿用了 snowflake 的设计,ID 组成结构:正数位
(占 1 比特)+ 时间戳
(占 41 比特)+ 机器ID
(占 5 比特)+ 机房ID
(占 5 比特)+ 自增值
(占 12 比特),总共 64 比特组成的一个 Long 类型。
Leaf-snowflake
不同于原始 snowflake 算法地方,主要是在 workId 的生成上,Leaf-snowflake
依靠Zookeeper
生成workId
,也就是上边的机器ID
(占 5 比特)+ 机房ID
(占 5 比特)。Leaf
中 workId 是基于 ZooKeeper 的顺序Id
来生成的,每个应用在使用 Leaf-snowflake 时,启动时都会都在 Zookeeper 中生成一个顺序 Id,相当于一台机器对应一个顺序节点,也就是一个 workId。
Leaf-snowflake
启动服务的过程大致如下:
启动 Leaf-snowflake 服务,连接 Zookeeper,在 leaf_forever 父节点下检查自己是否已经注册过(是否有该顺序子节点)。
如果有注册过直接取回自己的 workerID(zk 顺序节点生成的 int 类型 ID 号),启动服务。
如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的 workerID 号,启动服务。
但Leaf-snowflake
对 Zookeeper 是一种弱依赖关系,除了每次会去 ZK 拿数据以外,也会在本机文件系统上缓存一个workerID
文件。一旦 ZooKeeper 出现问题,恰好机器出现故障需重启时,依然能够保证服务正常启动。
启动Leaf-snowflake
模式也比较简单,起动本地 ZooKeeper,修改一下项目中的leaf.properties
文件,关闭leaf.segment模式
,启用leaf.snowflake
模式即可。
leaf.segment.enable=false#leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/xin-master?useUnicode=true&characterEncoding=utf8
评论