前言
分布式ID生成策略基本要求就是全局不重复,最好还能递增,长度较短,性能高,可用性强。关于相关的实现方案有很多,本文着重使用美团开源的分布式ID生成解决方案——Leaf。
关于Leaf,美团官方的介绍文档主要如下,强烈建议阅读文章大致了解Leaf的工作流程与原理,这对本文后续的源码解析有很大的帮助。
本系列Leaf源码解析部分按照使用的方式也分为号段模式和snowflake模式两篇文章,本文就来着重研究号段模式的源码实现。
本文的Leaf源码注释地址:https://github.com/MrSorrow/Leaf
I. 导入项目
Leaf由Maven构建,源码地址:https://github.com/Meituan-Dianping/Leaf
首先先Fork官方仓库到自己的仓库,我的源码注释版:https://github.com/MrSorrow/Leaf
下载源码,导入IDEA,导入成功依赖下载完成后项目结构大致如下:
II. 测试号段模式
「创建数据库表」
DROP TABLE IF EXISTS `leaf_alloc`;
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '业务key',
`max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',
`step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',
`description` varchar(256) DEFAULT NULL COMMENT '业务key的描述',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库维护的更新时间',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
「开启号段模式」
leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=1234
leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=
「测试号段模式」
开启号段模式并配置好数据库连接后,点击启动 leaf-server
模块的 LeafServerApplication
,将服务跑起来。
浏览器输入http://localhost:8080/api/segment/get/leaf-segment-test来获取分布式递增id;
监控号段模式:http://localhost:8080/cache
数据库表:
III. 号段模式源码解析
正式进入源码前,再强烈建议阅读官方的两篇博客,对Leaf的号段模式工作模式有个大致的理解。
我们从http://localhost:8080/api/segment/get/leaf-segment-test入口来分析。该请求会交由 com.sankuai.inf.leaf.server.LeafController
处理:
@Autowired
SegmentService segmentService;
/**
* 号段模式获取id
* @param key 对应数据库表的biz_tag
* @return
*/
@RequestMapping(value = "/api/segment/get/{key}")
public String getSegmentID(@PathVariable("key") String key) {
// 核心是segmentService的getId方法
return get(key, segmentService.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());
}
可以看到主要是调用 SegmentService
的 getId(key)
方法。key
参数其实就是路径上对应的 leaf-segment-test
,也就是数据库对应的 biz_tag
。 getId(key)
方法返回的是 com.sankuai.inf.leaf.common.Result
对象,封装了 id
和 状态 status
:
public class Result {
private long id;
private Status status;
// getter and setter....
}
public enum Status {
SUCCESS,
EXCEPTION
}
创建SegmentService
我们进入 SegmentService
类中,再调用 getId(key)
方法之前,我们先看一下 SegmentService
类的实例化构造函数逻辑。可以看到:
package com.sankuai.inf.leaf.server;
@Service("SegmentService")
public class SegmentService {
private Logger logger = LoggerFactory.getLogger(SegmentService.class);
IDGen idGen;
DruidDataSource dataSource;
/**
* 构造函数,注入单例SegmentService时,完成以下几件事:
* 1. 加载leaf.properties配置文件解析配置
* 2. 创建Druid dataSource
* 3. 创建IDAllocDao
* 4. 创建ID生成器实例SegmentIDGenImpl并初始化
* @throws SQLException
* @throws InitException
*/
public SegmentService() throws SQLException, InitException {
// 1. 加载leaf.properties配置文件
Properties properties = PropertyFactory.getProperties();
// 是否开启号段模式
boolean flag = Boolean.parseBoolean(properties.getProperty(Constants.LEAF_SEGMENT_ENABLE, "true"));
if (flag) {
// 2. 创建Druid 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();
// 3. 创建Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);
// 4. 创建ID生成器实例SegmentIDGenImpl
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
// 初始化SegmentIDGenImpl(加载db的tags至内存cache中,并开启定时同步更新任务)
if (idGen.init()) {
logger.info("Segment Service Init Successfully");
} else {
throw new InitException("Segment Service Init Fail");
}
} else {
// ZeroIDGen一直返回id=0
idGen = new ZeroIDGen();
logger.info("Zero ID Gen Service Init Successfully");
}
}
/**
* 根据key获取id
* @param key
* @return
*/
public Result getId(String key) {
return idGen.get(key);
}
/**
* 获取号段模式id生成器SegmentIDGenImpl
* @return
*/
public SegmentIDGenImpl getIdGen() {
if (idGen instanceof SegmentIDGenImpl) {
return (SegmentIDGenImpl) idGen;
}
return null;
}
}
SegmentService
类的构造函数,主要完成以下几件事:
- 加载
leaf.properties
配置文件,并解析配置 - 创建
Druid
数据源对象dataSource
- 创建
IDAllocDao
接口实例IDAllocDaoImpl
- 创建ID生成器实例
SegmentIDGenImpl
并初始化
① 解析leaf.properties配置文件
通过 PropertyFactory
读取了 leaf.properties
配置文件并进行解析。其中所以的key-value配置信息最终封装为 Properties
中。
/**
* 加载leaf.properties配置文件中配置信息
*/
public class PropertyFactory {
private static final Logger logger = LoggerFactory.getLogger(PropertyFactory.class);
private static final Properties prop = new Properties();
static {
try {
prop.load(PropertyFactory.class.getClassLoade