雪花算法学习

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

背景

在大自然雪花(snowflake)形成过程中,会形成不同的结构分支,所以说不存在两片完全一样的雪花,表示生成的id如雪花般独一无二。雪花算法最早是twitter内部使用的分布式环境下的唯一分布式ID生成算法。

分布式ID的常见解决方案

UUID

Java自带的生成一串唯一随机36位字符串(32个字符串+4个“-”)的算法。它可以保证唯一性,且据说够用N亿年,但是其业务可读性差,无法有序递增。

SnowFlake

雪花算法

https://github.com/twitter-archive/snowflake

UidGenerator

百度开源的分布式ID生成器,基于雪花算法实现

https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

Leaf

美团开源的分布式ID生成器,能保证全局唯一,趋势递增,但需要依赖关系数据库、Zookeeper等中间件

https://tech.meituan.com/MT_Leaf.html

雪花算法优点

1.系统环境ID不重复

能满足高并发分布式系统环境ID不重复,比如大家熟知的分布式场景下的数据库表的ID生成。

2.生成效率极高

在高并发,以及分布式环境下,除了生成不重复 id,每秒可生成百万个不重复 id,生成效率极高。

3.保证基本有序递增

基于时间戳,可以保证基本有序递增,很多业务场景都有这个需求。

4.不依赖第三方库

不依赖第三方的库,或者中间件,算法简单,在内存中进行。

雪花算法缺点

依赖服务器时间,服务器时钟回拨时可能会生成重复 id。

实现原理

Snowflake 结构是一个 64bit 的 int64 类型的数据。如下:

img

就是生成一个的 64 位的 long 类型的唯一 id,主要分为如下4个部分组成:

1)1位保留 (基本不用)

1位标识:由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0,所以这第一位都是0。

2)41位时间戳

接下来 41 位存储毫秒级时间戳,41位可以表示241-1个毫秒的值,转化成单位年则是:(241−1)/(1000∗60∗60∗24∗365)=69年 。

41位时间戳 :也就是说这个时间戳可以使用69年不重复,大概可以使用 69 年。

注意:41位时间截不是存储当前时间的时间截,而是存储时间截的差值“当前时间截 – 开始时间截”得到的值。

这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的,一般设置好后就不要去改变了!

因为,雪花算法依赖服务器时间,服务器时钟回拨时可能会生成重复 id

3)10位机器

10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId,最多可以部署 2^10=1024 台机器。

这里的5位可以表示的最大正整数是2^5−1=31,即可以用0、1、2、3、….31这32个数字,来表示不同的datecenterId,或workerId。

4) 12bit序列号

用来记录同毫秒内产生的不同id,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。

理论上雪花算法方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

上面只是一个将 64bit 划分的通用标准,一般的情况可以根据自己的业务情况进行调整。例如目前业务只有机器10台左右预计未来会增加到三位数,并且需要进行多机房部署,QPS 几年之内会发展到百万。

那么对于百万 QPS 平分到 10 台机器上就是每台机器承担百级的请求即可,12 bit 的序列号完全够用。对于未来会增加到三位数机器,并且需要多机房部署的需求我们仅需要将 10 bits 的 work id 进行拆分,分割 3 bits 来代表机房数共代表可以部署8个机房,其他 7bits 代表机器数代表每个机房可以部署128台机器。那么数据格式就会如下所示:

img

示例

go 中雪花算法的库

go get github.com/bwmarrin/snowflake

实现

package main
​
import (
    "fmt"
    "github.com/bwmarrin/snowflake"
    "time"
)
​
var node *snowflake.Node
​
func Init(startTime string, machineID int64) (err error) {
    var st time.Time
    st, err = time.Parse("2006-01-02 15:04:05", startTime)//需要格式化
    if err != nil {
        fmt.Println(err)
        return
    }
​
    snowflake.Epoch = st.UnixNano() / 1e6
    node, err = snowflake.NewNode(machineID)
    if err != nil {
        fmt.Println(err)
        return
    }
​
    return
}
​
func GenID() int64 {
    return  node.Generate().Int64()// 生成64位雪花 ID
}
​
func main() {
    if err := Init("2006-01-02 15:04:05", 1); err != nil {
        fmt.Println("Init() failed, err = ", err)
        return
    }
    id := GenID()
    fmt.Println(id)
}
运行结果

4.png

可能遇到的问题

将一个自定义的日期格式,如"2023-1-19 16:00",转为时间戳:

报错:

err: parsing time "2023-1-19 16:00" as "2023-1-19 16:00": cannot parse "-1-19 16:00" as ...
原因

对layout解析过程中, src/pkg/time/format.g中定义了格式常量(年月日等),对常量涵盖的年月日和其他的年月日解析不同,自定义的layout可能导致解析有问题。 比如在解析年份时,如果年份不等于2006,会被解析为stdDay,那么在解析别的时间的时候年被当成2,后面的解析自然就出问题了。

解决

如,constant定义的年份为2006(或06),月为1月(January/1/01/Jan)..,那么在进行自定义layout时,按照constant中已经定义的就可以正常解析了

    stdLongMonth             = iota + stdNeedDate  // "January"
    stdMonth                                       // "Jan"
    stdNumMonth                                    // "1"
    stdZeroMonth                                   // "01"
    stdLongWeekDay                                 // "Monday"
    stdWeekDay                                     // "Mon"
    stdDay                                         // "2"
    stdUnderDay                                    // "_2"
    stdZeroDay                                     // "02"
    stdUnderYearDay                                // "__2"
    stdZeroYearDay                                 // "002"
    stdHour                  = iota + stdNeedClock // "15"
    stdHour12                                      // "3"
    stdZeroHour12                                  // "03"
    stdMinute                                      // "4"
    stdZeroMinute                                  // "04"
    stdSecond                                      // "5"
    stdZeroSecond                                  // "05"
    stdLongYear              = iota + stdNeedDate  // "2006"
    stdYear                                        // "06"
    stdPM                    = iota + stdNeedClock // "PM"
    stdpm                                          // "pm"
    stdTZ                    = iota                // "MST"
    stdISO8601TZ                                   // "Z0700"  // prints Z for UTC
    stdISO8601SecondsTZ                            // "Z070000"
    stdISO8601ShortTZ                              // "Z07"
    stdISO8601ColonTZ                              // "Z07:00" // prints Z for UTC
    stdISO8601ColonSecondsTZ                       // "Z07:00:00"
    stdNumTZ                                       // "-0700"  // always numeric
    stdNumSecondsTz                                // "-070000"
    stdNumShortTZ                                  // "-07"    // always numeric
    stdNumColonTZ                                  // "-07:00" // always numeric
    stdNumColonSecondsTZ                           // "-07:00:00"
    stdFracSecond0                                 // ".0", ".00", ... , trailing zeros included
    stdFracSecond9                                 // ".9", ".99", ..., trailing zeros omitted

将layout修改为里面定义了的年月日时分秒,如2006-01-02 15:04:05

posted @ 2023-01-19 16:36  TomiokapEace  阅读(115)  评论(0编辑  收藏  举报