详解雪花算法 snowflake 生成分布式 ID

在大量的业务场景中,在将数据插入数据库前,需要一个自增的 ID,同时又不会重复。比如:春节期间的购票网站,短时间内会有大量的购票订单涌入系统;现在流行的各大社交网站,出现热点事件时,会有许多的报道评论等,产生大量的消息。这些订单、消息需要一个全局唯一 ID 进行标识,以更好地支持业务,尤其是在高并发场景下。

snowflack 算法就是在分布式系统下全局唯一 ID 的典型解决方案。

1. 分布式ID

分布式 ID 基本要求
  1. 全局唯一性:分布式ID既然是业务数据的唯一标识,那么ID自身的唯一性是最基本的要求;
  2. 趋势递增:由于分布式ID同时作为插入数据的主键ID插入数据,而许多数据库的索引是基于B+树,非趋势递增会导致页分裂,从而影响性能;
  3. 单调递增:保证上一个 ID 的大小一定小于下一个 ID,对于某些有排序需求的场景有一定的必要性,比如 IM 消息触达到端,或者就是单纯的有需要基于 ID 进行排序的需求。
  4. 信息安全:如果 ID 可以被简单的枚举出来,那么有潜在的数据安全问题。并且如果是订单号的场景,通过订单号的简单相减就预估出当天的订单量的话,会导致商业数据泄漏。

2. snowflake 算法介绍

Snowflake 雪花算法是 Twitter 开源的分布式 ID 生成算法。 首先确定,分布式 ID 是64个bit位,并分割为4个部分。

第1位 占用1bit,符号位,永远为0;

第2位开始的41位 占用41bit,表示生成分布式 ID 请求时的时间戳毫秒值,大约可以使用69年;

中间10位 标识节点ID,其中5位标识数据中心的 ID,再用5位标识机器实例的 ID,最多可以表示 2^10=1024个节点;

最后12位 是循环自增,这意味着在1ms内一个节点最多可以生成 2^12=4096 个有效 ID

3. GO 实现 snowflake

因为第1个bit位是符号位,固定为0,不用考虑,从第2个bit位开始实现

3.1 时间戳 为了延长分布式 ID 的使用时间,这里的时间戳选择一个相对于更晚的某个时间的毫秒值增量,例如以2024-01-24开始,把请求时间相对于该时间的偏移量作为 ID 的时间戳。 我们定义一个 twepoch 变量,即 2024-01-24 00:00:00 的时间戳毫秒值,使用请求的时间戳毫秒值减去这个twepoch即得到 ID 的时间戳

go 复制代码
const (
   timestampBits        = 41
   workerIdBits         = 10
   sequenceBits         = 12
   timestampShift       = workerIdBits + sequenceBits
   workIdShift          = sequenceBits
   sequenceMask         = 1<<sequenceBits - 1
   workIdMax      int64 = -1 ^ (-1 << workerIdBits)
)

var ( 
/* 
start time: 2024-01-24 
*/ 
twepoch = time.Date(2024, 1, 24, 0, 0, 0, 0, time.UTC).UnixMilli() 
)

/*
get the newest timestamp relative to twepoch
*/
func getNewestTimestamp(lastTimestamp int64) int64 {
   return lastTimestamp - twepoch
}

分布式 ID 一共64位,而时间戳位于从第2位开始的41个bit位,而右侧还有10位节点 ID 和12位递增序号,共22位,只要将得到的时间戳再向左位移22个bit位即得到时间戳在分布式 ID 的位置。同理,节点 ID 右侧有12位的序号,因此将节点 ID 向左位移12个bit位即得到节点 ID 的位置 在最终的结果中,我们会看到返回的 ID 是通过如下的位运算得到的:

go 复制代码
id = (ts << timestampShift) | (n.workId << workIdShift) | (n.sequence)

其中,timestampShift 时间戳的位移量(节点位数+序号位数),workIdShift 是节点 ID 的位移量(序号位数),ts 是相对于twepoch的时间戳毫秒值增量

3.2 节点 ID

为了简单,我们将 datacenter idmachine id 合并为 节点 ID 即 work id,与 timestampsequence id 不同的是,节点 ID并不是在程序运行期间生成的,而是在部署节点就确定了。我们可以在初始化读取配置等方式确定节点 ID。 定义了一个生成分布式 ID 的结构体,在初始化的时候,传入节点 ID,需要判断节点 ID 的范围是否在0到最大值之间

go 复制代码
type Node struct {
   lastTimestamp int64
   workId        int64
   sequence      int64

   lock sync.Mutex
}

// Init returns a new snowflake node that can be used to generate snowflake
func Init(workId int64) (*Node, error) {
   n := Node{}
   n.workId = workId
   if n.workId < 0 || n.workId > workIdMax {
      return nil, errors.New("Node number must be between 0 and " + strconv.FormatInt(workIdMax, 10))
   }
   return &n, nil
}

3.3 自增的序号 ID 如果请求的时间戳小于最后时间戳,发生了时间回拨,此时抛出异常; 如果请求的时间戳等于最后时间戳,那么序号 ID 只需要在上一个 ID的基础上+1即可,在该情况下,还需要考虑到上一个ID已经达到了最大值(12位全为1),那么接下来需要从0开始,并且时间戳要加上1ms。

go 复制代码
func (n *Node) NextId() (id int64, err error) {
   n.lock.Lock()
   defer n.lock.Unlock()
   now := time.Now().UnixMilli()
   // 发生时钟回拨
   if now < n.lastTimestamp {
      return id, fmt.Errorf("clock callback occurs")
   }

   if now == n.lastTimestamp {
      n.sequence = (n.sequence + 1) & sequenceMask
      if n.sequence == 0 {
         // 并发量太高,序列号溢出
         now = wait(n.lastTimestamp)
      }
   } else {
      n.sequence = 0
   }
   n.lastTimestamp = now
   ts := getNewestTimestamp(n.lastTimestamp)
   id = (ts << timestampShift) | (n.workId << workIdShift) | (n.sequence)
   return id, nil
}
   
   
func wait(lastTimestamp int64) int64 {
   now := time.Now().UnixMilli()
   if now <= lastTimestamp {
      time.Sleep(time.Millisecond * time.Duration(lastTimestamp-now+1))
   }
   return now
}

sequenceMask 是序列号掩码,也就是序列号最大值,即12个1。

3.4 完整代码 因为生成分布式 ID,会有并发场景,所以需要加锁

go 复制代码
package snowflake

import (
   "errors"
   "fmt"
   "strconv"
   "sync"
   "time"
)

/*
@author: shg
@since: 2024/1/24 11:34 PM
@mail: shgang97@163.com
*/

/*
0-41位毫秒级时间戳-10位节点id-12位序列号
*/

const (
   timestampBits        = 41
   workerIdBits         = 10
   sequenceBits         = 12
   timestampShift       = workerIdBits + sequenceBits
   workIdShift          = sequenceBits
   sequenceMask         = 1<<sequenceBits - 1
   workIdMax      int64 = -1 ^ (-1 << workerIdBits)
)

var (
   /*
      start time: 2024-01-24
   */
   twepoch = time.Date(2024, 1, 24, 0, 0, 0, 0, time.UTC).UnixMilli()
)

type Node struct {
   lastTimestamp int64
   workId        int64
   sequence      int64

   lock sync.Mutex
}

// Init returns a new snowflake node that can be used to generate snowflake
func Init(workId int64) (*Node, error) {
   n := Node{}
   n.workId = workId
   if n.workId < 0 || n.workId > workIdMax {
      return nil, errors.New("Node number must be between 0 and " + strconv.FormatInt(workIdMax, 10))
   }
   return &n, nil
}

func (n *Node) NextId() (id int64, err error) {
   n.lock.Lock()
   defer n.lock.Unlock()
   now := time.Now().UnixMilli()
   // 发生时钟回拨
   if now < n.lastTimestamp {
      return id, fmt.Errorf("clock callback occurs")
   }

   if now == n.lastTimestamp {
      n.sequence = (n.sequence + 1) & sequenceMask
      if n.sequence == 0 {
         // 并发量太高,序列号溢出
         now = wait(n.lastTimestamp)
      }
   } else {
      n.sequence = 0
   }
   n.lastTimestamp = now
   ts := getNewestTimestamp(n.lastTimestamp)
   id = (ts << timestampShift) | (n.workId << workIdShift) | (n.sequence)
   return id, nil
}

func wait(lastTimestamp int64) int64 {
   now := time.Now().UnixMilli()
   if now <= lastTimestamp {
      time.Sleep(time.Millisecond * time.Duration(lastTimestamp-now+1))
   }
   return now
}

/*
get the newest timestamp relative to twepoch
*/
func getNewestTimestamp(lastTimestamp int64) int64 {
   return lastTimestamp - twepoch
}

3.5 测试一下

go 复制代码
var wg sync.WaitGroup

func main() {
    // 初始化
    node, _ := snowflake.Init(1)
    fmt.Println("***************************** start *****************************")
    for i := 0; i < 10; i++ {
       wg.Add(1)
       li := i
       go func() {
          defer wg.Done()
          id, _ := node.NextId()
          fmt.Printf("第%d协程生成的id:%d,二进制为:%b\n", li, id, id)
       }()
    }
    wg.Wait()
    fmt.Println("***************************** end *****************************")

}

运行结果如下图,其中红色框为节点 ID 部分,左侧为相对时间戳,右侧为序列号 ID

测试下性能:

go 复制代码
func main() {
    start := time.Now().UnixMilli()
    for i := 0; i < 50000; i++ {
       _, _ = node.NextId()
    }
    t := time.Now().UnixMilli() - start
    fmt.Printf("生成50000个分布式id耗时:%d ms", t)
}
// 生成50000个分布式id耗时:18 ms

4. snowflake 算法缺陷

雪花算法对机器时间是强依赖的,如果机器上时钟回拨,会导致生成的 ID 重复或者服务会处于不可用状态。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。官方对于此并没有给出解决方案,而是简单的抛错处理,这样会造成在时间被追回之前的这段时间服务不可用。

go 复制代码
 now := time.Now().UnixMilli()
   // 发生时钟回拨
   if now < n.lastTimestamp {
      return id, fmt.Errorf("clock callback occurs")
   }

总结

本文首先介绍实际业务场景,引入分布式ID,并简单介绍了分布式ID的基本要求,之后介绍了 snowflake 雪花算法并实现了 snowflake 算法,详细讲解了实现思路,并在最后介绍了 snowflake 算法缺陷。

相关推荐
processflow流程图15 分钟前
分布式kettle调度平台v6.4.0新功能介绍
分布式
全栈开发圈24 分钟前
干货分享|分布式数据科学工具 Xorbits 的使用
分布式
运维&陈同学3 小时前
【zookeeper01】消息队列与微服务之zookeeper工作原理
运维·分布式·微服务·zookeeper·云原生·架构·消息队列
时差9533 小时前
Flink Standalone集群模式安装部署
大数据·分布式·flink·部署
菠萝咕噜肉i3 小时前
超详细:Redis分布式锁
数据库·redis·分布式·缓存·分布式锁
只因在人海中多看了你一眼6 小时前
分布式缓存 + 数据存储 + 消息队列知识体系
分布式·缓存
zhixingheyi_tian9 小时前
Spark 之 Aggregate
大数据·分布式·spark
求积分不加C10 小时前
-bash: ./kafka-topics.sh: No such file or directory--解决方案
分布式·kafka
nathan052910 小时前
javaer快速上手kafka
分布式·kafka
谭震鸿14 小时前
Zookeeper集群搭建Centos环境下
分布式·zookeeper·centos