GO实现雪花算法

3,274 阅读5分钟

雪花算法

0(1位符号位,且始终为0)|时间戳(41位)|工作机器id(10位)|序列号(12位)

雪花算法长度为64bit,且有如下特征:

  1. 最高位是符号位,始终为0
  2. 41位的时间戳,精度位毫秒级,可使用69年
  3. 10位的机器码,支持1024个节点
  4. 12位的计数序列号,自增。同一节点,同一毫秒最多产生4096个序号

学习目标:

  1. 如何实现一个简单的雪花算法
  2. 为什么只能用69年
  3. 为什么雪花算法的长度是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))
}