分布式ID生成器

如果在一个分布式系统中生成类似于MySQL自增ID这样不断增大, 同时又不会重复的ID,使用传统的单机做法就存在问题了,因为 在分布式系统中,每秒产生10W+的ID,ID每次增1的方式就变得不可取了。 考虑到多节点的情况,分布式并发的情况下可能出现问题。

分布式系统中,对于ID的设计是在保证不重复的情况下又可以排序, 设计思路是在ID中带有一些时间信息,这样即使进行了分库分表,分节点 生成,也能以时间顺序对这些值进行排序。

通用的做法是使用twitter的snowflake算法。snowflake由以下部分组成:

未使用	                41位时间戳                        datacenter_id     sequence_id
  ↓                        ↓                                  ↓                 ↓
|  0 | 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0 | 00000 | 00000| 0000 0000 0000|
 
                                                                     ↑	
                                                                  worker_id

整个数值的长度是64位,int64类型,被划分为四部分,不含开头的第一个bit,因为这个bit是符号位。用41位来表示收到请求时的时间戳,单位为毫秒,接着五位来表示数据中心的id,后边五位表示机器实例ID,最后的12位为循环自增id。达到111111111111后会归0。

这样的机制可以支持在同一台机器上,同一毫秒产生2^12=4096条消息。一秒产生409.6万个id。从值域的角度来讲是完全够用了。

数据中心加上节点id共有10位,可以支持每个数据中心32台机器,所有数据中心共1024个节点。表示timestamp的41位可以支持使用69年。如果使用1970年的计数方式,那么跑到2039年就不能用了,这里的timestamp是相对一个时间的增量,也就是偏移量。

worker_id的分配

timestampdatacenter_idworker_idsequence_id这四个字段中, timestampsequence_id是由程序生成的, datacenter_idworker_id可以真实的对应部署节点和集群的编号, 也可以由程序生成,不同点在于,一旦启动,值就不可更改了,从而解决冲突问题。 如果可以随意修改,可能被不慎修改成重复的值,造成生成的id有冲突。

一般情况下,既可以通过制定id也可以通过程序来给定。

在hubble中也是采用类似于snowflake的生成方法,总体思路就是时间戳+网卡,对于复杂的可能加入了DCE及命名空间等,如果有长短要求的情况下还会有生成短id等不同的处理方式。

开源实现

github.com/bwmarrin/snowflake是一个用go语言实现的snowflake包,对各位的定义为:

1位符号位,41位时间戳,10位nodeid,12位序列。和标准的snowflake完全一致。

该包的使用

package main

import (
    "fmt"
    "os"

    "github.com/bwmarrin/snowflake"
)

func main() {
    n, err := snowflake.NewNode(1)
    if err != nil {
        println(err)
        os.Exit(1)
    }
    
    for i :=0; i < 3; i++ {
        id := n.Generate()
        fmt.Println("id", id)
        fmt.Println(
            "node: ", id.Node(),
            "step: ", id.Step(),
            "time: ", id.Time(),
            "\n",
        )
  }
}

这个库的可定制部分主要是这些长度:

    // Epoch is set to the twitter snowflake epoch of Nov 04 2010 01:42:54 UTC
    // You may customize this to set a different epoch for your application.
    Epoch int64 = 1288834974657

    // Number of bits to use for Node
    // Remember, you have a total 22 bits to share between Node/Step
    NodeBits uint8 = 10

    // Number of bits to use for Step
    // Remember, you have a total 22 bits to share between Node/Step
    StepBits uint8 = 12

Epoch时间戳,起始时间。NodeBits机器编号位,StepBits自增序列长。

总结

现在snowflake各类语言都有自己的实现,重要的是领会其中的逻辑,根据实际情况定制自己的id,同时还要考虑生成效率,多思考,例如,为什么传统的seq不行呢,在分布式系统中传统序列有什么缺陷。