🦆分布式ID的需求背景
我试图把这个内容说得直白一些。
分布式ID服务,是指在我们的分布式服务群里,有那么一个(组)服务,啥也不干只管给其他系统发放ID。
当然这只是目前来说的最终状态,我们不妨回到过去,看看分布式ID是怎么一步一步走到今天这个样子的。
- 上古时代,ID由数据库自增提供
一开始,我们的ID根本不会考虑分布式的问题,对于每个实体类型,完全依赖数据库的自增ID安排就好了。
不过随着业务量增大,一个问题出现了:自增ID是服务于单库的,对于大并发写入,就捉襟见肘了。
-
分库自增ID的方案
当单点不满足我们的写入性能要求,我们就分库好了,通过一些特定的哈希规则,把数据分别写到不同的数据库实例里。
那怎么保证几个库的自增ID不重叠呢?原来,数据库自增ID还能设置起始值和步长。
不过,这个方案需要你前期就对业务规模发展有一个准确的估计,因为后续继续扩容节点,是一件十分麻烦的事。
有多麻烦?有人可以自己想想吗?
-
再后来,干脆把自增ID这个能力独立出来
上面提到的方案,不管是单点还是分库,都是每种实体类型管理自己的ID序列。比如订单、商品、售后记录这些,分别有自己的ID序列,它们分别在不同的平行世界。
后来人们发现,我们产生了一种新的需求:不同的实体类型公用一个ID序列。
这个时候,就回到了本文顶部的那张图。业务流程会变成:
- 服务A需要新增一条记录
- 服务A向IDs申请一个id
- 服务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,而是总结了整个过程中的一些感悟:
-
号段模式注意边界切换
实现号段模式时,每次会提前申请一批ID。当系统分配完一批ID,而这时又有新的请求进来时,我们称之为边界。
为了避免这个时候去操作数据库而产生“卡顿”,我采取了提前申请下一组ID的措施。就是当存量ID使用超过80%时,异步提前申请下一批ID做后备,当到达边界时,可以做到无缝切换。
-
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里去返回(这个希望有人可以给出解决方案。)
-