分布式ID算法&实现

时间:2022-07-24
本文章向大家介绍分布式ID算法&实现,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、为什么需要分布式ID

1、跨机房部署

如果数据库是跨机房部署,分布式ID是必须的,不然后续做数据分析和统计、跨机房路由会踩大坑。

2、海量数据

如果数据量可能会超出数据库自增ID类型最大值, 分布式ID也是必然面对的。

二、分布式ID的需求有哪些

先看下功能性需求

1、全局唯一

即不管是哪个机房生成的,全局必须唯一,不能和其它机房产生的值冲突

2、单调递增

保证下一个ID一定大于上一个ID

3、具有一定的安全性

像订单号这种场景,不能让人一看最新的ID就知道你每天的订单量了

非功能性要求:

1、高可用

这个一个基础的功能,如果挂掉,会影响很多业务

2、高吞吐量

上面的功能性需求看每家公司自己的业务场景。

三、常用算法有

1、snowflake(雪花)算法

生成一个64bit的数字,数字被划分成多个段:时间戳、机器编码、序号。

优点:

  • 整个ID是趋势递增的。
  • 高吞吐量。
  • 可以根据自身业务需求灵活调整bit划分。

缺点:

  • 依赖机器时钟,如果机器时钟回拨,会导致ID重复。
  • 在分布式环境下,每台机器上的时钟可能有偏差,有时候会出现不是全局递增的情况。

2、基于数据库

一般基于数据库,充分利用MySQL自增ID的机制。

3、UUID

UUID是Universally Unique Identifier的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符,UUID是16字节128位长的数字,通常以36字节的字符串表示。

优点:

  • 本地生成ID,不需要进行远程调用,时延低,性能高。

缺点:

  • UUID过长,16字节128位,通常以36长度的字符串表示,很多场景不适用,比如用UUID做数据库索引字段。
  • 没有排序,无法保证趋势递增。
  • 因为UUID是随机的,在保存数据的时候不是特别高效,查询也不方便。

这种方案一般用的比较少,除非不用存储在数据库中。

四、实现方案

上面讲了大概的理论,我们看下目前比较著名的实现方案。

4.1、美团的Leaf-Segment

实现了Leaf-segment和Leaf-snowflake方案。

4.1.1 Leaf-segment方案

以MySQL举例,利用给字段设置auto_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

表结构如下:

Create Table Tickets64 (
id bigint(20) unsigned not null auto_increment,
stub char(1),
Primary KEY(id),
UNIQUE KEY ix_stub(stub)
)Engine = InnoDB;

如果业务比较多,可以使用多张表。

刚开始使用如下:

begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;

这个方案的缺点如下:

1)、每次都要操作1次数据库,性能不高。

2)强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。

经过改进,方案如下:

数据库设计如下

+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag     | varchar(128) | NO   | PRI |                   |                             |
| max_id      | bigint(20)   | NO   |     | 1                 |                             |
| step        | int(11)      | NO   |     | NULL              |                             |
| desc        | varchar(256) | YES  |     | NULL              |                             |
| update_time | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+

重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。

部署架构

1、增加一层proxy

服务不再直连mysql,而是改为连proxy server,这样就可以做隔离了,后续存储改为其它数据库,系统升级也比较方便。

2、增加步长

每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step。

4.1.2 Leaf-snowflake方案

Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。面对这一问题,美团提供了 Leaf-snowflake方案。

启动步骤如下:

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

时钟问题解决方案:

  1. 若写过,则用自身系统时间与leaf_forever/{self}节点记录时间做比较,若小于leaf_forever/{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
  2. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
  3. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。
  4. 否则认为本机系统时间发生大步长偏移,启动失败并报警。
  5. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

4.2 百度的uid-generator

github地址:https://github.com/baidu/uid-generator

分配方式:

  • sign(1bit) 固定1bit符号标识,即生成的UID为正数。
  • delta seconds (28 bits) 当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年
  • worker id (22 bits) 机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
  • sequence (13 bits) 每秒下的并发序列,13 bits可支持每秒8192个并发。

worder id由数据库来保存,个人觉得会是一个瓶颈,并且用完即弃,如果机器经常启动,感觉也会很快用完。

单机并发只有8192,看具体业务场景,当然可以再扩充,可以让worder id变短些。

4.3 微信的seqsvr

适用于面向每个用户的场景,像用户数据同步等。

实现思路是:预分配+分号段共享存储+存储和缓存分离

在容灾方面,先是用主从架构

后来再采用动态路由表的方案来解决配置不一致的问题,

细节就不在这里聊了,有兴趣的同学可以百度下:微信序列号生成器架构设计及演变