分布式系统中如何有效的生成唯一ID

导读:本篇文章讲解 分布式系统中如何有效的生成唯一ID,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

背景:

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。比如支付流水号、订单号、用户编号等等。当数据量大到我们不得不分库分表处理时,如何保证ID的唯一性就成了问题。


解决:

方案一:数据库生成

在单机系统中,我们一般采用数据库自增的方式生成唯一ID主键,显然分布式环境下不适用普通的自增方式。
但是可以设置数据库自增的步长。mysql默认的自增步长为1。也就是:1、2、3、4……顺序下去。假设在分布式系统环境中,我们预先知道需要部署N台数据库实例,那么可以将这N台数据库实例的步长设置为N,然后分别设置每台数据库实例的自增起始点,第一台设置为1,第二台设置为2……第N台设置为N。这样就可以使得每台机器生成的主键ID错开,保证唯一性!

这种方案的优缺点如下:
优点:

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。

缺点:

  • 扩展性很差,数据库实例水平扩展比较困难。一开始就得确定好机器数。

方案二:类snowflake方案

这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:
在这里插入图片描述
41-bit的时间可以表示(1L<<41)/(1000L360024*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

这种方式的优缺点是:

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

方案三:美团Leaf方案

Leaf是美团基础研发平台推出的一个分布式ID生成服务,它以springboot jar的方式提供服务。它支持两种唯一ID生成方式:

  • segment
  • snowflake

segment:

在使用数据库的方案上,做了如下改变: – 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。 – 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

数据库表设计如下:
在这里插入图片描述
重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step,大致架构如下图所示:
在这里插入图片描述
更新号段的sql如下:

Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit

但是这种模式有一些缺点

  • TP999数据波动大,当号段使用完之后还是会卡在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
  • DB宕机会造成整个系统不可用。
  • Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,不适用于订单ID等生成场景。容易被爬数据以及猜测出当天的订单量。不是很安全,只能适用于某些数据不是很敏感的场景。

美团对前两条稍作了优化(双Buffer优化):
Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。

为此,我们希望DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。

详细实现如下图所示:
在这里插入图片描述
采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。

  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

简单来说:原本是一次性取1000条ID,当用完后再去取,但是在这个取期间万一网络不稳定或者其他原因。就很容易导致堵塞。所以优化成:一开始取1000条ID放到segment的第一个buffer中,当用到超过10%,也就是100条时,就异步去数据库取下一段1000条ID。放到segment的第二个buffer里。当第一个buffer用完时就切换到第二个buffer当中。同样当第二个buffer用到10%时,再异步去取1000条放到第一个buffer,依次循环。这样就解决了上面所说的两个问题。

需要注意的是,当leaf发号服务宕机重启后,leaf会直接舍弃当前号段,直接跳到下一号段。

snowflake方案

由于Leaf-segment方案生成的ID不具备随机性,在某些场景,如订单、支付流水等数据下则需要采用snowflake方案。
在这里插入图片描述
Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:

  • 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
  • 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
  • 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

在这里插入图片描述
弱依赖ZooKeeper
除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA

解决时钟问题
因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。
在这里插入图片描述
参见上图整个启动流程图


Leaf示例:

1、下载Leaf项目代码:Github地址


2、以独立服务的方式运行Leaf

配置Leaf:

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

Leaf Server的配置都在leaf-server/src/main/resources/leaf.properties中
在这里插入图片描述

Segment模式

如果使用号段模式,需要建立DB表,并配置leaf.jdbc.url, leaf.jdbc.username, leaf.jdbc.password

如果不想使用该模式配置leaf.segment.enable=false即可。

创建数据表

CREATE DATABASE leaf;
-- 库可以使用自己的,只需后面在leaf.properties文件中指定
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');

配置相关数据项
在leaf.properties中配置leaf.jdbc.url, leaf.jdbc.username, leaf.jdbc.password参数

Leaf默认使用的是mysql5.xx版本,如果你用的是mysql8.xx版本,那么还需修改如下:

1、leaf.properties文件增加leaf.jdbc.driver-class-name属性:

leaf.jdbc.driver-class-name=com.mysql.cj.jdbc.Driver

2、父工程中的poem文件中修改mysql依赖版本

<mysql-connector-java.version>8.0.23</mysql-connector-java.version>

3、 com.sankuai.inf.leaf.server.Constants 增加字段

    //新增字段
    public static final String LEAF_JDBC_DRIVER_CLASS_NAME = "leaf.jdbc.driver-class-name";

4、com.sankuai.inf.leaf.server.service.SegmentService 读取该字段

dataSource.setDriverClassName(properties.getProperty(Constants.LEAF_JDBC_DRIVER_CLASS_NAME));

在这里插入图片描述

Snowflake模式

算法取自twitter开源的snowflake算法。

如果不想使用该模式配置leaf.snowflake.enable=false即可。

配置zookeeper地址
在leaf.properties中配置leaf.snowflake.zk.address,配置leaf 服务监听的端口leaf.snowflake.port。

运行Leaf:

打包服务:

git clone git@github.com:Meituan-Dianping/Leaf.git
//按照上面的号段模式在工程里面配置好
cd leaf
mvn clean install -DskipTests
cd leaf-server

运行服务:
注意:首先得先配置好数据库表或者zk地址

mvn方式

mvn spring-boot:run  

脚本方式

sh deploy/run.sh

测试:

#segment
curl http://localhost:8080/api/segment/get/leaf-segment-test
#snowflake
curl http://localhost:8080/api/snowflake/get/test

监控页面:
号段模式:http://localhost:8080/cache

当然,为了追求更高的性能,需要通过RPC Server来部署Leaf 服务,那仅需要引入leaf-core的包,把生成ID的API封装到指定的RPC框架中即可。


3、使用leaf-starter注解来启动leaf

打包服务:

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>

配置leaf.properties到你的classpath下面

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,并使用api

//EnableLeafServer 开启leafserver
@SpringBootApplication
@EnableLeafServer
public class LeafdemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(LeafdemoApplication.class, args);
	}
}
//直接使用 spring注入
public class T {
    @Autowired
    private SegmentService segmentService;
    @Autowired
    private SnowflakeService snowflakeService;
}


结束:

参考: Leaf—美团点评分布式ID生成系统

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/99044.html

(0)

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!