雪花算法
0(1位符号位,且始终为0)|时间戳(41位)|工作机器id(10位)|序列号(12位)
雪花算法长度为64bit,且有如下特征:
- 最高位是符号位,始终为0
- 41位的时间戳,精度位毫秒级,可使用69年
- 10位的机器码,支持1024个节点
- 12位的计数序列号,自增。同一节点,同一毫秒最多产生4096个序号
学习目标:
- 如何实现一个简单的雪花算法
- 为什么只能用69年
- 为什么雪花算法的长度是64位
算法实现
package main
import (
"fmt"
"time"
)
/*
参考 https://www.cnblogs.com/efish/p/snow-arithmetic.html
*/
/*
雪花算法组成部分:
共64bit
0(1位,且始终为0)|时间戳(41位)|工作机器id(10位)|序列号(12位)
*/
var (
machineID int64 //机器id
sn int64 //序列号
lastTimeStamp int64 //记录上次的时间戳(毫秒级)
)
func init() {
lastTimeStamp = time.Now().UnixNano() / 1e6
}
func SetMachineID(mid int64) {
machineID = mid << 12
}
func GetSnowflakeID() int64 {
// 单位为毫秒
curTimeStamp := time.Now().UnixNano() / 1e6
if curTimeStamp == lastTimeStamp {
sn++
//序列号为12位, 2^12 = 4096个
if sn > 4095 {
//序列号超出,则重置序列号。这也意味着每毫秒最多能生成4096个id值
time.Sleep(time.Millisecond)
curTimeStamp = time.Now().UnixNano() / 1e6
lastTimeStamp = curTimeStamp //顺便更新下上次的时间戳
sn = 0
}
//与运算 对应位全为1时,则为1.否则为0
rightBinValue := curTimeStamp & 0x1FFFFFFFFFF
rightBinValue <<= 22
//或运算 对应位全为0时,则为0。否则为1
id := rightBinValue | machineID | sn
return id
} else if curTimeStamp > lastTimeStamp {
sn = 0
lastTimeStamp = curTimeStamp
rightBinValue := curTimeStamp & 0x1FFFFFFFFFF
rightBinValue <<= 22
return rightBinValue | machineID | sn
}
return 0
}
func main() {
SetMachineID(111)
fmt.Println(GetSnowflakeID() >> 22) //右移22位,就可以得到时间戳(毫秒级)
}
为什么只能用69年?
首先,先回顾下时间戳的概念:时间戳是指格林威治时间自1970年1月1日(00:00:00 GMT)至当前时间的总秒数。
雪花算法中,定义时间戳是41位,且是毫秒级。先算一个值: 2^41 = 2199023255552。这个算出来的值,代表了是多少毫秒。
我们计算下一个数据,每天有 24 ×60×60=86400秒。则69年有69×365×86400=2193868800秒。转换为毫秒也就是2193868800000。
我们会发现,相差的值,和上面的2^41的值接近。如果超过69年,那么41位表示的时间戳就不够存放了。
为什么雪花算法是64位
雪花算法的长度之所以为64位,取决于程序中int类型表示的最大的值,也就是int64所能存储的最大值。让我们用程序验证。
package main
import "fmt"
func main() {
var max int64
max = 0xFFFFFFFFFFFFFFFF //16进制的数,每一位占用4字节。int64共字节,64/4=16个F
fmt.Println(max)
}
# 报错: constant 18446744073709551615 overflows int64
上面的代码溢出了,怎么回事?原因其实是第一位字节是符号位,总是为0。但是我们上面第一位是F,占用4字节,且为1111。我们把首位改成0,即0111,那么它对应的16进制的数为多少呢?我们看以下数据:
2进制 |0000|0001|0010|0011|0100|0101|0110|0111|1000|1001|1010|1011|1100|1101|1110|1111|
10进制 |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |10 |11 |12 |13 |14 |15 |
16进制 |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |A |B |C |D |E |F |
0111代表16进制的7,现在我们改一下上面的程序
func main() {
var max int64
max = 0x7FFFFFFFFFFFFFFF //16进制的数,每一位占用4字节。int64共字节,64/4=16个F
fmt.Println(max)
}
# 没有报错,输出结果为9223372036854775807
如何解决已损失的年数
既然是从1970年计算时间戳,现在是2020年了,已经过去了50年了。也就是说,上面的程序还能使用19年!白白浪费了50年。这,真的有点...
解决方案也很简单,我们可以手动设置设置一个时间戳的计时起始点,然后我们程序的运行的时间戳都减去该起始点,得到一个新值,去表示那41位的时间戳。
修改后的代码如下:
package main
import (
"fmt"
"time"
)
/*
参考 https://www.cnblogs.com/efish/p/snow-arithmetic.html
*/
/*
雪花算法组成部分:
共64bit
0(1位,且始终为0)|时间戳(41位)|工作机器id(10位)|序列号(12位)
*/
var (
Epoch int64 = 1597075200000 //2020年8月11号0:00 时刻的毫秒级时间戳
machineID int64 //机器id
sn int64 //序列号
lastTimeStamp int64 //记录上次的时间戳(毫秒级)
)
func init() {
lastTimeStamp = time.Now().UnixNano()/1e6 - Epoch
}
func SetMachineID(mid int64) {
machineID = mid << 12
}
func GetSnowflakeID() int64 {
// 单位为毫秒
curTimeStamp := time.Now().UnixNano()/1e6 - Epoch
if curTimeStamp == lastTimeStamp {
sn++
//序列号为12位, 2^12 = 4096个
if sn > 4095 {
//序列号超出,则重置序列号。这也意味着每毫秒最多能生成4096个id值
time.Sleep(time.Millisecond)
curTimeStamp = time.Now().UnixNano()/1e6 - Epoch
lastTimeStamp = curTimeStamp //顺便更新下上次的时间戳
sn = 0
}
//与运算 对应位全为1时,则为1.否则为0
rightBinValue := curTimeStamp & 0x1FFFFFFFFFF
rightBinValue <<= 22
//或运算 对应位全为0时,则为0。否则为1
id := rightBinValue | machineID | sn
return id
} else if curTimeStamp > lastTimeStamp {
sn = 0
lastTimeStamp = curTimeStamp
rightBinValue := curTimeStamp & 0x1FFFFFFFFFF
rightBinValue <<= 22
return rightBinValue | machineID | sn
}
return 0
}
func main() {
SetMachineID(111)
id := GetSnowflakeID()
fmt.Printf("id : %d\n", id)
fmt.Printf("时间戳: %d\n", id>>22+Epoch) //时间戳
fmt.Printf("机器码: %d\n", id&(-1^(-1<<10)<<12)>>12) //机器码
fmt.Printf("序列号: %d\n", id&(-1^(-1<<12)))
fmt.Println("机器码:", id&(2<<21-1)>>12)
fmt.Println("序列号:", id&(2<<12-1))
}