不吹嘘,不夸张,项目中用到ID生成的场景确实挺多。好比业务要作幂等的时候,若是没有合适的业务字段去作惟一标识,那就须要单独生成一个惟一的标识,这个场景相信你们不陌生。html
不少时候为了图方即可能就是写一个简单的ID生成工具类,直接开用。作的好点的可能单独出一个Jar包让其余项目依赖,作的很差的颇有可能就是Copy了N份同样的代码。git
单独搞一个独立的ID生成服务很是有必要,固然咱们也不必本身作造轮子,有现成开源的直接用就是了。若是人手够,不差钱,自研也能够。github
今天为你们介绍一款美团开源的ID生成框架Leaf,在Leaf的基础上稍微扩展下,增长RPC服务的暴露和调用,提升ID获取的性能。redis
Leaf介绍Leaf 最先期需求是各个业务线的订单ID生成需求。在美团早期,有的业务直接经过DB自增的方式生成ID,有的业务经过redis缓存来生成ID,也有的业务直接用UUID这种方式来生成ID。以上的方式各自有各自的问题,所以咱们决定实现一套分布式ID生成服务来知足需求。算法
目前Leaf覆盖了美团点评公司内部金融、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。在4C8G VM基础上,经过公司RPC方式调用,QPS压测结果近5w/s,TP999 1ms。spring
snowflake模式snowflake是Twitter开源的分布式ID生成算法,被普遍应用于各类生成ID的场景。Leaf中也支持这种方式去生成ID。sql
使用步骤以下:数据库
修改配置leaf.snowflake.enable=true开启snowflake模式。apache
修改配置leaf.snowflake.zk.address和leaf.snowflake.port为你本身的Zookeeper地址和端口。bootstrap
想必你们很好奇,为何这里依赖了Zookeeper呢?
那是由于snowflake的ID组成中有10bit的workerId,以下图:
图片
通常若是服务数量很少的话手动设置也没问题,还有一些框架中会采用约定基于配置的方式,好比基于IP生成wokerID,基于hostname最后几位生成wokerID,手动在机器上配置,手动在程序启动时传入等等方式。
Leaf中为了简化wokerID的配置,因此采用了Zookeeper来生成wokerID。就是用了Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。
若是你公司没有用Zookeeper,又不想由于Leaf去单独部署Zookeeper的话,你能够将源码中这块的逻辑改掉,好比本身提供一个生成顺序ID的服务来替代Zookeeper。
segment模式segment是Leaf基于数据库实现的ID生成方案,若是调用量不大,彻底能够用Mysql的自增ID来实现ID的递增。
Leaf虽然也是基于Mysql,可是作了不少的优化,下面简单的介绍下segment模式的原理。
首先咱们须要在数据库中新增一张表用于存储ID相关的信息。
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用于区分业务类型,好比下单,支付等。若是之后有性能需求须要对数据库扩容,只须要对biz_tag分库分表就行。
max_id表示该biz_tag目前所被分配的ID号段的最大值。
step表示每次分配的号段长度。
下图是segment的架构图:
图片
从上图咱们能够看出,当多个服务同时对Leaf进行ID获取时,会传入对应的biz_tag,biz_tag之间是相互隔离的,互不影响。
好比Leaf有三个节点,当test_tag第一次请求到Leaf1的时候,此时Leaf1的ID范围就是1~1000。
当test_tag第二次请求到Leaf2的时候,此时Leaf2的ID范围就是1001~2000。
当test_tag第三次请求到Leaf3的时候,此时Leaf3的ID范围就是2001~3000。
好比Leaf1已经知道本身的test_tag的ID范围是1~1000,那么后续请求过来获取test_tag对应ID时候,就会从1开始依次递增,这个过程是在内存中进行的,性能高。不用每次获取ID都去访问一次数据库。
问题一这个时候又有人说了,若是并发量很大的话,1000的号段长度一下就被用完了啊,此时就得去申请下一个范围,这期间进来的请求也会由于DB号段没有取回来,致使线程阻塞。
放心,Leaf中已经对这种状况作了优化,不会等到ID消耗完了才去从新申请,会在还没用完以前就去申请下一个范围段。并发量大的问题你能够直接将step调大便可。
问题二这个时候又有人说了,若是Leaf服务挂掉某个节点会不会有影响呢?
首先Leaf服务是集群部署,通常都会注册到注册中心让其余服务发现。挂掉一个不要紧,还有其余的N个服务。问题是对ID的获取有问题吗? 会不会出现重复的ID呢?
答案是没问题的,若是Leaf1挂了的话,它的范围是1~1000,假如它当前正获取到了100这个阶段,而后服务挂了。服务重启后,就会去申请下一个范围段了,不会再使用1~1000。因此不会有重复ID出现。
Leaf改造支持RPC若是大家的调用量很大,为了追求更高的性能,能够本身扩展一下,将Leaf改形成Rpc协议暴露出去。
首先将Leaf的Spring版本升级到5.1.8.RELEASE,修改父pom.xml便可。
<spring.version>5.1.8.RELEASE</spring.version> 复制代码
而后将Spring Boot的版本升级到2.1.6.RELEASE,修改leaf-server的pom.xml。
<spring-boot-dependencies.version>2.1.6.RELEASE</spring-boot-dependencies.version> 复制代码
还须要在leaf-server的pom中增长nacos相关的依赖,由于咱们kitty-cloud是用的nacos。同时还须要依赖dubbo,才能够暴露rpc服务。
<dependency> <groupId>com.cxytiandi</groupId> <artifactId>kitty-spring-cloud-starter-nacos</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.cxytiandi</groupId> <artifactId>kitty-spring-cloud-starter-dubbo</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> </dependency> 复制代码
在resource下建立bootstrap.properties文件,增长nacos相关的配置信息。
spring.application.name=LeafSnowflake dubbo.scan.base-packages=com.sankuai.inf.leaf.server.controller dubbo.protocol.name=dubbo dubbo.protocol.port=20086 dubbo.registry.address=spring-cloud://localhost spring.cloud.nacos.discovery.server-addr=47.105.66.210:8848 spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.discovery.server-addr} 复制代码
Leaf默认暴露的Rest服务是LeafController中,如今的需求是既要暴露Rest又要暴露RPC服务,因此咱们抽出两个接口。一个是Segment模式,一个是Snowflake模式。
Segment模式调用客户端
/** * 分布式ID服务客户端-Segment模式 * * @做者 尹吉欢 * @我的微信 jihuan900 * @微信公众号 猿天地 * @GitHub https://github.com/yinjihuan * @做者介绍 http://cxytiandi.com/about * @时间 2020-04-06 16:20 */ @FeignClient("${kitty.id.segment.name:LeafSegment}") public interface DistributedIdLeafSegmentRemoteService { @RequestMapping(value = "/api/segment/get/{key}") String getSegmentId(@PathVariable("key") String key); } 复制代码
Snowflake模式调用客户端
/** * 分布式ID服务客户端-Snowflake模式 * * @做者 尹吉欢 * @我的微信 jihuan900 * @微信公众号 猿天地 * @GitHub https://github.com/yinjihuan * @做者介绍 http://cxytiandi.com/about * @时间 2020-04-06 16:20 */ @FeignClient("${kitty.id.snowflake.name:LeafSnowflake}") public interface DistributedIdLeafSnowflakeRemoteService { @RequestMapping(value = "/api/snowflake/get/{key}") String getSnowflakeId(@PathVariable("key") String key); } 复制代码
使用方能够根据使用场景来决定用RPC仍是Http进行调用,若是用RPC就@Reference注入Client,若是要用Http就用@Autowired注入Client。
最后改造LeafController同时暴露两种协议便可。
@Service(version = "1.0.0", group = "default") @RestController public class LeafController implements DistributedIdLeafSnowflakeRemoteService, DistributedIdLeafSegmentRemoteService { private Logger logger = LoggerFactory.getLogger(LeafController.class); @Autowired private SegmentService segmentService; @Autowired private SnowflakeService snowflakeService; @Override public String getSegmentId(@PathVariable("key") String key) { return get(key, segmentService.getId(key)); } @Override 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()); } }