一、Leaf
在当今日益数字化的世界里,软件系统的开发已经成为了几乎所有行业的核心。然而,随着应用程序的规模不断扩大,以及对性能和可扩展性的需求不断增加,传统的软件架构和设计模式也在不断地面临挑战。其中一个主要挑战就是如何有效地处理分布式环境中的唯一标识问题。这正是分布式ID
的重要性所在。
分布式ID
的实现方式有多种多样,常见的包括 UUID
、Snowflake
算法、Twitter
的 Snowflake
算法、基于数据库的自增长ID
等。每种方式都有其适用的场景和优缺点。
比如常见的 UUID
, 标准型式包含32
个16
进制数字,以连字号分为五段,形式为8-4-4-4-12
的36
个字符,优点是性能非常高,本地生成,没有网络消耗,但缺点也显而易见,首先不易于存储,UUID
太长,16
字节128
位,通常以36
长度的字符串表示,很多场景不适用。其次信息不安全,基于MAC
地址生成UUID
的算法可能会造成MAC
地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。也不适合作为DB
的主键。MySQL
官方有明确的建议主键要尽量越短越好。
基于数据库的自增长ID
的方式,实现起来非常简单,并且ID是单向自增顺序的,但缺点也很明显,过度依赖于 DB 数据库,在并发量高的情况下数据库成为了性能瓶颈。
基于Snowflake
算法的方式,可以解决上述提到的问题,并且稳定性和灵活性都非常高,但强依赖于机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
既然如此,那下面我们来认识更强大的分布式ID
生成器 Leaf
,它是美团开源的分布式 ID
生成器,旨在解决分布式系统中的唯一标识生成问题,确保在分布式环境下生成的 ID
具有全局唯一性、顺序性和高性能。
Leaf
实现了Leaf-segment
和Leaf-snowflake
两种方案。
Leaf-segment
是一种基于数据库的分布式 ID
生成方案,原始基于数据库的自增长ID
方案,每次获取ID
都得读写一次数据库,造成数据库压力大,该方案利用proxy server
批量获取,每次获取一个segment
(step
决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。各个业务不同的发号需求用biz_tag
字段来区分,每个biz-tag
的ID
获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag
分库分表就行。
Leaf-snowflake
方案完全沿用snowflake
方案的bit
位设计,对于workerID
的分配,使用Zookeeper
持久顺序节点的特性自动对snowflake
节点配置wokerID
,对于时钟回拨问题,解决方案如下:
更多介绍可以参考官方信息:
官方介绍地址:https://tech.meituan.com/2017/04/21/mt-leaf.html
github:https://github.com/Meituan-Dianping/Leaf.git
下面一起来实践下Leaf
的使用。
首先拉取 Leaf
SpringBoot
封装依赖源码:
shell
git clone -b feature/spring-boot-starter https://github.com/Meituan-Dianping/Leaf.git
shell
cd leaf
使用 Maven
将 Leaf
打到本地仓库中
shell
mvn clean install -Dmaven.test.skip=true
打包成功后,可以创建一个 SpringBoot
项目,在 pom
中加入下面依赖:
xml
<dependency>
<artifactId>leaf-boot-starter</artifactId>
<groupId>com.sankuai.inf.leaf</groupId>
<version>1.0.1-RELEASE</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</exclusion>
<exclusion>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
二、Leaf-segment 方式使用
首先创建leaf
使用的数据库:
sql
CREATE DATABASE leaf
创建ID
规则表:
sql
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;
写入两个 biz_tag
:
sql
insert into leaf_alloc(biz_tag, max_id, step, description) values('test1', 1, 2000, '测试1');
insert into leaf_alloc(biz_tag, max_id, step, description) values('test2', 1, 2000, '测试2');
项目中加入leaf
和数据库配置:
yml
leaf:
name: test1
segment:
enable: true
url: jdbc:mysql://localhost:3306/leaf?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
username: root
password: root
生成ID
测试:
java
@Slf4j
@SpringBootTest
class LeafIdApplicationTests {
@Resource
private SegmentService segmentService;
@Test
void contextLoads() {
// 生成 1000 个ID
StopWatch sw = new StopWatch();
sw.start();
for (int i = 0; i < 1000; i++) {
long id1 = segmentService.getId("test1").getId();
long id2 = segmentService.getId("test2").getId();
log.info("id1: {}, id2: {}", id1, id2);
}
sw.stop();
log.info(sw.prettyPrint());
}
}
可以看到在约 0.178
秒的时间,为两个业务场景生成了 1000
个ID
。
三、Leaf-snowflake 方式使用
这种模式依赖于 Zookeeper
,所以在实验前你需要有一个运行中的 Zookeeper
服务。
这种模式操作ZK
使用 curator
,因此需要引入 curator
的依赖:
xml
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
在配置文件中开启Leaf-snowflake
模式:
yml
leaf:
name: test1
segment:
enable: true
url: jdbc:mysql://localhost:3306/leaf?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
username: root
password: root
snowflake:
enable: true
address: 127.0.0.1
port: 2181
生成ID
测试:
java
@Slf4j
@SpringBootTest
class LeafIdApplicationTests {
@Resource
private SegmentService segmentService;
@Resource
private SnowflakeService snowflakeService;
@Test
void contextLoads() {
// 生成 1000 个ID
StopWatch sw = new StopWatch();
sw.start();
for (int i = 0; i < 1000; i++) {
long id1 = snowflakeService.getId("test1").getId();
long id2 = snowflakeService.getId("test2").getId();
log.info("id1: {}, id2: {}", id1, id2);
}
sw.stop();
log.info(sw.prettyPrint());
}
}
可以看到相比于上面数据库模式,仅需要约 0.0234105
秒,性能更高,而且做到ID
不是顺序+1
式增长。