本篇博客的视频教程首发于 Youtube:科技小飞哥,加入 电报粉丝群 获得最新视频更新和问题解答。

背景

在互联网的业务系统中,涉及到各种各样的ID,这些ID需要保证全局唯一。我们称之为分布式ID,分布式ID需要满足 唯一性、趋势递增性、高可用性、高性能等特点。

snowflake算法,也叫雪花算法,是其中的一种分布式ID生成方案。是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

讲解雪花算法前,我们先概述一下分布式ID有哪些生成方案。

分布式ID生成方案

分布式ID有以下几种生成方式:

UUID

算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。

优点:本地生成,生成简单,性能好,没有高可用风险 缺点:长度过长,存储冗余,且无序不可读,查询效率低

数据库自增ID

使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用多台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。

优点:数据库生成的ID绝对有序,高可用实现方式简单 缺点:需要独立部署数据库实例,成本高,有性能瓶颈

Redis生成ID

Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。

优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排序的结果很有帮助。 缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作量比较大。

考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台 Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。各个 Redis 生成的 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

步长和初始值一定需要事先确定。使用 Redis 集群也可以防止单点故障的问题。 另外,比较适合使用 Redis 来生成每天从0开始的流水号。比如订单号 = 日期 + 当日自增长号。可以每天在 Redis 中生成一个 Key ,使用 INCR 进行累加。

snowflake算法

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

Twitter的snowflake分布式ID的算法是目前广泛使用的分布式ID算法,尽管有很多变种,比如位数的不同,时间片大小不同、node bit数放在最后等各种变种,但是主要思想还是来自于snowflake的思想。

Twitter在2010年儿童节的时候在官方博客上介绍了snowflake算法,内部用来表示每一条tweet。

snowflake

snowflake算法采用64bit存储ID, 最高位备用,暂时不使用。接下来的41 bit做时间戳,最小时间单位为毫秒。再接下来的10 bit做机器ID(worker id),然后最后12 bit在单位时间(毫秒)递增。

41 bit表示时间戳大约可以使用69年(2^41 -1), 为了尽可能的表示时间,时间戳可以从第一次部署的时候开始计算,比如2020-02-02 00:00:00, 这样69年内可以无虞。

10 bit区分机器,所以可以支持1024台机器。 你也可以把10bit分成两部分,一部分做数据中心的ID,一部分做机器的ID,比如55分的化,可以支持32个数据中心,每个数据中心最多可以支持32台机器。

12 bit自增值可以表示4096的ID,也就是说每台机器每毫秒最多产生4096个ID,这是它的最大性能。

正如前面所说,时间戳、机器ID、自增ID所占的位数可以根据你实际的情况做调整。

snowflake还有一个很好的特性就是基本保持顺序性,因为它的前几位是时间戳,可以对ID按照时间进行排序。

snowflake算法详解

golang核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var (
    // 可以设置成系统第一次上线的时间,单位:毫秒。占用41位,可以使用69年。
    Epoch int64 = 1288834974657

    // 实例的ID占用10位,最多支持1024个实例。
    NodeBits uint8 = 10

    // 步长占用12位,每一台实例上每一毫秒最多产生2^12=4096个不重复的数据。
    StepBits uint8 = 12
)

// 生成算法
func (n *Node) Generate() ID {

    n.mu.Lock()
    // 从设定的时间戳到现在经历的毫秒数
    now := time.Since(n.epoch).Nanoseconds() / 1000000
    //和上次记录一样,step加1,否则step重置为0
    if now == n.time {
        n.step = (n.step + 1) & n.stepMask

        if n.step == 0 {
            for now <= n.time {
                now = time.Since(n.epoch).Nanoseconds() / 1000000
            }
        }
    } else {
        n.step = 0
    }

    n.time = now
    // 时间戳 41 位 | node 10位 | step 12位
    r := ID((now)<<n.timeShift |
        (n.node << n.nodeShift) |
        (n.step),
    )

    n.mu.Unlock()
    return r
}

具体的bit可以根据自己的需求进行调整,比如既可以是(41,10,12),也可以是(41,6,16)…

完整的golang代码repo: snowflake github

优点:

  • 存储少, 8个字节
  • 可读性高
  • 性能好,可以中心化的产生ID,也可以独立节点生成

缺点:

  • 时间回拨会重复产生ID

时钟回拨

如果我们的场景需要解决时钟回拨的问题:

可以找2bit位作为时钟回拨位,发现有时钟回拨就将回拨位加1,达到最大位后再从0开始进行循环。 例如上图中的, 10 bit的机器号=>8 bit的机器号+2 bit的回拨位. 每次发现时钟回拨,就把回拨位+1.大于最大值(3)后重设为0.

如图所示,在同一个时间段,最多支持时钟回拨3次。 每次回拨后,时钟回拨位不同,导致最终生成的ID也不同。

snowflake_time

<全文完>