雪花算法学习
这是我参与「第五届青训营 」伴学笔记创作活动的第 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 类型的数据。如下:
就是生成一个的 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台机器。那么数据格式就会如下所示:
示例
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)
}
运行结果
可能遇到的问题
将一个自定义的日期格式,如"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