Spring Cloud微服务分布式ID生成方案(六)

478 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

前言

前一篇已经介绍过为什么要使用分布式ID,分布式ID要具有什么要的特性。因为文章中给出了实际项目中的代码,涉及到篇幅较长,所以分成了两篇来介绍。

今天要介绍的分布式ID的实现方式是美团的leaf、和滴滴的Tinyid。因为开发这两个项目的团队都没有讲项目发布到Maven仓库,因此无法通过Maven的镜像下载依赖。所以才会有先clone项目,安装到自己的本地仓库。如果以后能从Maven的镜像中下载依赖,请忽略clone项目的步骤。

美团Leaf

Leaf由美团开发。Leaf 最早期需求是各个业务线的订单ID生成需求。在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也有的业务直接用UUID这种方式来生成ID。以上的方式各自有各自的问题,因此我们决定实现一套分布式ID生成服务来满足需求。目前Leaf覆盖了美团点评公司内部金融、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。在4C8G VM基础上,通过公司RPC方式调用,QPS压测结果近5w/s,TP999 1ms。

Leaf 提供两种生成的ID的方式(号段模式和snowflake模式),可以同时开启两种方式,也可以指定开启某种方式(默认两种方式为关闭状态)。

Leaf可提供了一个基于spring boot的HTTP服务来获取ID。即可以独立部署成ID服务器,其他服务通过HTTP请求获取分布式ID。Leaf生成的分布式ID是序号,不是long长度的数字。

Leaf也可以通过starter注解启动。下面是通过stater注解启动的示例。

  • 克隆项目,切换分支,安装到本地仓库。
git clone git@github.com:Meituan-Dianping/Leaf.git
git checkout feature/spring-boot-starter
cd leaf
mvn clean install -Dmaven.test.skip=true 
  • 项目中引入依赖。
<dependency>
  <artifactId>leaf-boot-starter</artifactId>
  <groupId>com.sankuai.inf.leaf</groupId>
  <version>1.0.1-RELEASE</version>
</dependency>
  • 添加配置文件

项目的classpath下也就是在resources目录下新建配置文件leaf.properties

leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=false
#leaf.segment.url=
#leaf.segment.username=
#leaf.segment.password=

leaf.snowflake.enable=false
#leaf.snowflake.address=
#leaf.snowflake.port=

配置项说明

配置项含义默认值
leaf.nameleaf 服务名
leaf.segment.enable是否开启号段模式false
leaf.jdbc.urlmysql 库地址
leaf.jdbc.usernamemysql 用户名
leaf.jdbc.passwordmysql 密码
leaf.snowflake.enable是否开启snowflake模式false
leaf.snowflake.zk.addresssnowflake模式下的zk地址
leaf.snowflake.portsnowflake模式下的服务注册端口
  • 号段模式需要配置数据库。
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, 2000, 'Test leaf Segment Mode Get Id')
  • 在启动类上添加注解@EnableLeafServer
@SpringBootApplication
@EnableLeafServer
public class CalculateBizApplication {
    public static void main(String[] args) {
        SpringApplication.run(CalculateBizApplication.class, args);
    }
}
  • 需要生成ID的地方引入Bean即可。
@Autowired
private SegmentService segmentService;

public void xxxMethod(){
  // leaf-segment-test是表leaf_alloc的字段biz_tag
  segmentService.getId("leaf-segment-test").getId()
}

滴滴Tinyid

分布式id是需要单独启动一个分布式ID服务,可以通过http调用接口获取分布式id,也可以引入它提供的客户端获取id。

Tinyid生成的分布式id也是序列id,不是long长度的id。

  • 全局唯一的long型id

  • 趋势递增的id,即不保证下一个id一定比上一个大

  • 非连续性

  • 提供http和java client方式接入

  • 支持批量获取id

  • 支持生成1,3,5,7,9...序列的id

  • 支持多个db的配置,无单点

适用场景:只关心id是数字,趋势递增的系统,可以容忍id不连续,有浪费的场景 不适用场景:类似订单id的业务(因为生成的id大部分是连续的,容易被扫库、或者测算出订单量)

  • 克隆项目。
git clone https://github.com/didi/tinyid.git
  • 配置数据库 创建数据表,执行tinyid-server项目下的db.sql文件,创建表
CREATE TABLE `tiny_id_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
  `begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
  `max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
  `step` int(11) DEFAULT '0' COMMENT '步长',
  `delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
  `remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';

CREATE TABLE `tiny_id_token` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `token` varchar(255) NOT NULL DEFAULT '' COMMENT 'token',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '此token可访问的业务类型标识',
  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'token信息表';

INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
	(1, 'test', 1, 1, 100000, 1, 0, '2018-07-21 23:52:58', '2018-07-22 23:19:27', 1);

INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
	(2, 'test_odd', 1, 1, 100000, 2, 1, '2018-07-21 23:52:58', '2018-07-23 00:39:24', 3);


INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
	(1, '0f673adf80504e2eaa552f5d791b644c', 'test', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

INSERT INTO `tiny_id_token` (`id`, `token`, `biz_type`, `remark`, `create_time`, `update_time`)
VALUES
	(2, '0f673adf80504e2eaa552f5d791b644c', 'test_odd', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');
  • 修改clone下来的项目的数据库配置信息。
cd tinyid-server/src/main/resources/offline
vi application.properties
datasource.tinyid.names=primary

datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456

修改完成后,需要启动tinyid的项目tinyid-server。滴滴的分布式ID是需要独立部署它提供的服务,这个服务作为一个分布式ID服务,其他服务可以通过http请求或者它提供的客户端sdk获取分布式ID。

在自己的项目中引入依赖,这个依赖没有在mvn仓库中。需要通过自己刚才克隆的项目,通过mvn命令安装包本地仓库中。

<dependency>
    <groupId>com.xiaoju.uemc.tinyid</groupId>
    <artifactId>tinyid-client</artifactId>
    <version>${tinyid.version}</version>
</dependency>
  • 在自己项目的classpath中创建tinyid_client.properties
# 刚才启动的tinyid-server项目的地址和端口
tinyid.server=localhost:9999
# 表tiny_id_token中的token字段的值
tinyid.token=0f673adf80504e2eaa552f5d791b644c
  • 项目中使用示例。
// test是表tiny_id_info中的biz_type的值,和配置文件中`tinyid.token`是配套使用
Long id = TinyId.nextId("test");
List<Long> ids = TinyId.nextId("test", 10);

因为篇幅的问题所以分布式ID写了两篇,这两篇中都没有介绍其他的方式比如UUID、数据库自增长、redis的incry,因为大家都知道UUID作为数据库主键不合适、自己实现数据库自增长可能有点麻烦,需要care很多问题,主要这两篇文章中介绍的百度的uid-generator、美团的leaf、滴滴的tinyid,他们的实现方式就有数据库的方案,思路是一样的,而且毕竟是大厂,我们可以看源码学习甚至改造即可,但是没必要从开始重复造轮子,redis的方案了个人只在一些需要生成一些业务账号之类的进行使用过,实际生产上要用redis来生成主键,单靠他的incry可能还是需要再考虑一下。

几个分布式ID生成方案中,有直接生成long类型长度字符串,也有按序号生成的long字符串,可以根据自己实际的需要进行参考即可。