6

分布式ID生成方案-snowflake算法

 2 years ago
source link: https://www.leftpocket.cn/post/system_design/snowflake/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

原文地址:码农在新加坡的个人博客

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

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

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

分布式ID生成方案

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

算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成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核心代码如下:

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

<全文完>

欢迎关注我的微信公众号:码农在新加坡,有更多好的技术分享。

pic

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK