雪花算法和代码实现分析

最近在 GitHub 上看一个分布式定时器实现,每一个定时任务需要分配一个唯一 ID,项目使用的是雪花算法。于是找资料学习了以下雪花算法。

雪花算法

1
2
3
+--------------------------------------------------------------------------+
| 1 Bit Unused | 41 Bit Timestamp |  10 Bit NodeID  |   12 Bit Sequence ID |
+--------------------------------------------------------------------------+

总共 64 bit,8 Byte。

  • Unused,1 bit,无用。

  • Timestamp,41 bit,时间戳,精确到毫秒,可容纳 69 年。指的是相对于起始时间的的毫秒数,可以自定义起始时间。

  • NodeID,10 bit,工作机器ID(分布式),数量可达 1024。

  • Sequence ID 序列ID(同一毫秒内),数量可达 4096。

SnowFlake 算法同一毫秒内最多可以生成全局唯一ID的数量:4 百万(1024 X 4096 = 4194304)。

优点

  1. 支持分布式,全局唯一性。

  2. 递增性,利于 MySQL 索引的插入(前提是把该 ID 作为主键)。

    如果因为 MySQL 聚簇索引的数据是根据主键递增的,如果插入数据不是递增的,而是随机的,存在以下缺点:
    (1)查找插入位置耗费时间;
    (2)如果数据页面已满,插入新数据会导致页面分裂

  3. 高可用性,确保任何时候都能生成正确的 ID。

  4. 高性能,高并发的环境下依旧表现良好。

注意:

  1. 雪花算法是 64 bit,Go 直接用 uint64 存储即可;
  2. JavaScript 的 Number 的范围为 ±2^53,所以 JavaScript 中雪花算法的 ID 必须用 string 存储。

雪花算法实现

这里是在 GitHub 上找的一个 Go 实现的雪花算法。看完我感觉实现很巧妙,其实这也是我写这一篇文章想分享的原因。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// github.com/twitter-archive/snowflake

// 下面是默认的配置
var (
	// Epoch is set to the twitter snowflake epoch of Nov 04 2010 01:42:54 UTC in milliseconds
	// You may customize this to set a different epoch for your application.
    // 起始时间(距离 Nov 04 2010 01:42:54 UTC 的毫秒数)
	Epoch int64 = 1288834974657

	// NodeBits holds the number of bits to use for Node
	// Remember, you have a total 22 bits to share between Node/Step
	NodeBits uint8 = 10

	// StepBits holds the number of bits to use for Step
	// Remember, you have a total 22 bits to share between Node/Step
	StepBits uint8 = 12

	// DEPRECATED: the below four variables will be removed in a future release.
	mu        sync.Mutex
	nodeMax   int64 = -1 ^ (-1 << NodeBits)
	nodeMask        = nodeMax << StepBits
	stepMask  int64 = -1 ^ (-1 << StepBits)
	timeShift       = NodeBits + StepBits
	nodeShift       = StepBits
)

type Node struct {
    // 互斥锁,保证并发安全
	mu    sync.Mutex
	epoch time.Time
    // 这个 time 是精髓,表示上一次获取 id 所处的毫秒
	time  int64
	node  int64
	step  int64
	nodeMax   int64
	nodeMask  int64
	stepMask  int64
	timeShift uint8
	nodeShift uint8
}

// 核心代码其实就下面几行
// 代码值得学习,很巧妙  

func (n *Node) Generate() ID {
	n.mu.Lock()

	now := time.Since(n.epoch).Nanoseconds() / 1000000

    // 判断本次获取 ID 是否和上次处于同一毫秒
	if now == n.time {
        // step 表示本毫秒的下一个 ID
		n.step = (n.step + 1) & n.stepMask
        // 下一个 ID 为 0,说明已经循环了一次,本毫秒内没有 ID 可用了       
		if n.step == 0 {
            // 自旋直到进入新的毫秒
			for now <= n.time {
				now = time.Since(n.epoch).Nanoseconds() / 1000000
			}
		}
	} else {
        // 新毫秒的第一个 ID
		n.step = 0
	}

	n.time = now

    // 通过位运算来得到 ID,把时间和机器 ID 和该毫秒的第几个 ID 拼凑在一块
	r := ID((now)<<n.timeShift |
		(n.node << n.nodeShift) |
		(n.step),
	)

	n.mu.Unlock()
	return r
}

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import (
    "github.com/bwmarrin/snowflake"
    "time"

)

int main() {
	startTime := "2022-12-20"
	var machineID int64 = 1
	st, _ := time.Parse("2006-01-02", startTime)
	// 修改起始时间
	snowflake.Epoch = st.UnixNano() / 1000000
	node, _ := snowflake.NewNode(machineID)
	// 注意是 64 位
	var ID int64 = node.Generate().Int64()
	print(ID)
}

End

0%