🦆分布式ID的需求背景

我试图把这个内容说得直白一些。

分布式ID服务,是指在我们的分布式服务群里,有那么一个(组)服务,啥也不干只管给其他系统发放ID。

当然这只是目前来说的最终状态,我们不妨回到过去,看看分布式ID是怎么一步一步走到今天这个样子的。

  • 上古时代,ID由数据库自增提供

一开始,我们的ID根本不会考虑分布式的问题,对于每个实体类型,完全依赖数据库的自增ID安排就好了。

不过随着业务量增大,一个问题出现了:自增ID是服务于单库的,对于大并发写入,就捉襟见肘了。

  • 分库自增ID的方案

    当单点不满足我们的写入性能要求,我们就分库好了,通过一些特定的哈希规则,把数据分别写到不同的数据库实例里。

    那怎么保证几个库的自增ID不重叠呢?原来,数据库自增ID还能设置起始值和步长。

    不过,这个方案需要你前期就对业务规模发展有一个准确的估计,因为后续继续扩容节点,是一件十分麻烦的事。

有多麻烦?有人可以自己想想吗?

  • 再后来,干脆把自增ID这个能力独立出来

    上面提到的方案,不管是单点还是分库,都是每种实体类型管理自己的ID序列。比如订单、商品、售后记录这些,分别有自己的ID序列,它们分别在不同的平行世界。

    后来人们发现,我们产生了一种新的需求:不同的实体类型公用一个ID序列。

    这个时候,就回到了本文顶部的那张图。业务流程会变成:

    1. 服务A需要新增一条记录
    2. 服务A向IDs申请一个id
    3. 服务A把业务数据和id写到数据库

    想想这样做的好处是什么?

🦆业界最常用的两种实现

上面交代完需求背景,我们接下来实现一个分布式ID服务。

业界这类开源的项目比较多,我们今天并不会讲解开源项目,而是介绍一下两种常见的分布式ID生成实现。

🦆号段模式

是什么

号段模式和前面说到的历史方案,都是依赖数据库来管理ID生成与分配。区别在于,以往的实现里,没请求一个ID,都要产生一次数据库写入操作,注定性能上不去。

号段模式对此的改良,是一次性申请一个区间的ID,比如 [1,100)。如此一来,通过一次数据库写入,提供100次的ID请求,大大提升了性能指标。

怎么做

上面说到的原理,大家都能理解。

一般来说,我们需要提前在数据库里初始化一些信息。如

name current step
order 100 100
product 200 100

这个表格记录了每一种类型(order / product)序列,当前分配出去的最大ID(current), 以及当前序列每次分配的ID数量(step)。

那么,当order序列分配 100 时,ID池用完的情况下,再次向数据库申请一批(step个)ID。

此时,数据库记录会更新为

name current step
order 200 100

通过这个例子,我们很容易发现,一次数据库写入(更新)操作,可以应付 100 次的服务请求。
服务的性能指标理论上提升了100倍。

🦆snowflake

另一个值得介绍的分布式ID方案,就是大名鼎鼎的 snowflake算法。

背景&解决什么问题

snowflake是Twitter开源的分布式ID生成算法,它给一个long类型的每个bit都分别赋予了特定的含义。

具体到一个64位整型:

  • 第0位固定为0,表示非负数
  • 接下来41位来表示时间,单位是毫秒,2^41 可以存69年多一点
  • 接下来的10位,原理上说是5位表示机房,5位表示机器,在实际应用在,你可以赋予它不一样的含义,总的来说是用来区分机器的(或者说服务的实例)。这里共可以区分1024个节点,或者1024个容器实例。
  • 最后12位,是服务运行时,自己维护的一个序号,每次生成一个新的ID之后,就自己加一。这里的容量是4096。

综上,如果你采取上面的编排方案,意味着你在一个有1024个节点的分布式ID服务集群里,每毫秒能产生4096个不同的ID。

实际上不会有谁弄个1024个节点的ID服务,这个方案的并发上限非常高。

技术实现

snowflake有太多开源的实现,一般不需要我们自己手写。

不过我们还可以根据自己的需要,参考这个算法实现一套规模更小的 snowflake

比如我只需要32位的整形(有些语言就可怜的没法表达64位),

或者根据自己的需要调整每一段的位数,有的实现是时间容量更大,有的实现是节点数容纳更多,反正比较自由。

🦆自己手写两种实现时,碰到的一些细节问题记录

然后我试着按照自己对以上两种模式的理解,分别实现的号段模式snowflake

代码在这里 github地址

并不是要star,而是总结了整个过程中的一些感悟:

  1. 号段模式注意边界切换

    实现号段模式时,每次会提前申请一批ID。当系统分配完一批ID,而这时又有新的请求进来时,我们称之为边界。

    为了避免这个时候去操作数据库而产生“卡顿”,我采取了提前申请下一组ID的措施。就是当存量ID使用超过80%时,异步提前申请下一批ID做后备,当到达边界时,可以做到无缝切换。

  2. javascript专属问题

    因为我是使用ts开发,不可避免要面对js一些根源性的问题,比如没有没有64位整形。

    • 位运算左移的坑

      在实现snowflake时,就会用到左移操作 <<, 而在js的实现里,左移最多支持int32的范围,再大就溢出了。

      但是众所周知 js 的数字类型是遵从 IEEE 754 ,理论上可以支持很大的数字。

      所以这个是无解的,最后我使用了 Bigint 类型。

      关于 BigInt 的更多信息,请参考 BigInt[MDN]

    • 传输时,还是要变成字符串

      考虑到这个服务需要提供restful接口,在实际传输中,我还是把ID变成字符串来传输。

      main.get(name).then((id: bigint) => {
          res.end(JSON.stringify({
            id: id.toString(),
          }));
        })
      

      因为BigInt不能直接放到json里去返回(这个希望有人可以给出解决方案。)

原文地址