多种分布式id生成策略

1,886 阅读11分钟

前言

在如今的互联网应用中,数据量越来越大,导致某个表可能要占用很大的物理存储空间,为了解决该问题,使用数据库分片技术。将一个数据库进行拆分为多张相同的表,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。

基本原则

而分布式ID首先要保证的几个基本要求有:

  • 全局唯一
    这个最好理解,作为唯一标识,肯定不能出现重复,这是最基本的要求。

  • 趋势递增
    由于我们的分布式ID,是用来标识数据唯一性的,所以多数时候会被定义为主键或者唯一索引。而多数情况下我们使用的是MySQL数据库,存储引擎为innodb,innodb的默认索引结构为B树,对于BTree索引来讲,数据以自增顺序来写入的话,b+tree的结构不会时常被打乱重塑,存取效率是最高的,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

  • 单调递增
    保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。

  • 信息安全
    由于数据是递增的,所以,恶意用户的可以根据当前ID推测出下一个,非常危险,所以,我们的分布式ID尽量做到不易被破解。如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

  • 含时间戳
    方便在开发中快速了解这个id的生成时间。

可用性要求

在分布式id生成策略中,不仅仅要保证几个基本原则,还对可用性有着很强的要求。

  • 高可用
    发送一个生成id的请求,服务器就要保证99.9999%情况下创建一个可用的分布式id。比如说在一个业务场景下,不能因为id生成不了而造成插入数据操作全部不可用。

  • 低延迟
    发送一个生成id的请求,就要快速的返回这个id,不能说是等个好几秒才生成。

  • 高QPS
    比如有个活动,10万个人来同时进行下单操作,那么服务器要顶住压力同时创建10万个id。

说完这些分布式id要保证的硬性要求,下面来介绍几种分布式id的生成策略。

UUID

UUID由以下几部分的组合:

  • 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
  • 时钟序列。
  • 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

组合成8-4-4-12的36个字符串,如:3db1064b-2356-4a95-9429-3cc477879bd3.

  • 优点:

    • 本地生成,没有网络消耗。
  • 缺点:

    • 首先分布式id一般都会作为主键,但是安装mysql官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐。
    • 既然分布式id是主键,然后主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键地城的b+树进行很大的修改,这一点很不好。
    • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

数据库自增

当服务使用的数据库只有单裤单表时,可以使用多主模式利用数据库的auto_increment来生成全局唯一递增id。

设置好两台服务器的起始值和补偿,来进行id生成。

  • 优点:
    • 简单,无需任何程序附加操作
    • 保持订场增量
  • 缺点:
    • 高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限
    • 水平扩展困难,严重依赖数据库,扩容需要停机

redis

redis单线程的,所以操作为原子操作。利用incr/incrby命令实现原子性的自增可以生成唯一的递增ID。

例如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,步长为5。

那么生成的id为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25

  • 优点:
    • 性能较好,生成的id自增
  • 缺点:
    • 依赖于redis,水平扩容困难,配置复杂
    • 需要考虑持久化问题

twitter的snowflake算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。

下图是snowflake的基本组成。

  • 最高位是符号位,始终为0,不可用。
  • 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。
  • 10位的机器标识,10位的长度最多支持部署1024个节点。
  • 12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。

具体源码地址:github.com/beyondfengy…

具体实现的话可以使用hutool工具包中的IdUtil工具类来生成。

//参数1为终端ID
//参数2为数据中心ID
Snowflake snowflake = IdUtil.createSnowflake(1, 1);
long id = snowflake.nextId();
  • 优点:
    • 毫秒数在高位,自增序列在低位,整个id都是趋势递增的。
    • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成id的性能也是非常高的。
    • 可以根据自身业务特性分配bit未,非常灵活
  • 缺点:
    • 依赖机器时钟,如果机器时钟回拨,会导致重复id生成
    • 在单机上是递增的,但是由于设计到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况

美团的Leaf-segment

美团的Leaf-segment方案,实际上是在上面介绍的数据库自增ID方案上的一种改进方案,它可生成全局唯一、全局有序的ID,可以用于:事务版本号、IM聊天中的增量消息、全局排序等业务中。

美团的Leaf-segment对数据库自增ID方案做了如下改变:

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

数据库表设计如下:

CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '' COMMENT '用来区分业务',
  `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '表示该biz_tag目前所被分配的ID号段的最大值',
  `step` int(11) NOT NULL COMMENT '表示每次分配的号段长度',
  `description` varchar(256)  DEFAULT NULL COMMENT '描述',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。

读写数据库的频率从1减小到了1/step,大致架构如下图所示:

  • 优点:
    • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景
    • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求
    • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务
    • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来
  • 缺点:
    • ID号码不够随机,能够泄露发号数量的信息,不太安全
    • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺
    • DB宕机会造成整个系统不可用
    • Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,但不适用于订单ID生成场景。比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的

美团的Leaf-snowflake

严格来说,Leaf-snowflake方案是Twittersnowflake改进版,它完全沿用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号,需要解决时钟回退的问题。

详细启动流程:

参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

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

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警。如下:

 //发生了回拨,此刻时间小于上次发号时间
 if (timestamp < lastTimestamp) {
  			  
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                	//时间偏差大小小于5ms,则等待两倍时间
                    wait(offset << 1);//wait
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                       //还是小于,抛异常并上报
                        throwClockBackwardsEx(timestamp);
                      }    
                } catch (InterruptedException e) {  
                   throw  e;
                }
            } else {
                //throw
                throwClockBackwardsEx(timestamp);
            }
        }
 //分配ID      

小结

以上Snowflake、美团的Leaf、还有文中没有提到百度的uid-generator都是现在比较常用方案,大都是基于Snowflake再做一定的优化。不过具体用哪个方案还是要根据具体的业务、并发量等等做选择,不能说是无脑的直接取用Leaf或者uid-generator。我目前所在做的项目也是用的公司自己封装的一套类似于Snowflake的一种生成策略,因为项目的并发数和数据量并没有互联网项目的那么大,所以说选择合适适用于自己项目的方案才是最OK的。

完!

都看到着了,点个赞点个关注再走呗~

参考:

tech.meituan.com/2017/04/21/…