高并发雪花算法 | 青训营笔记

167 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记

1.雪花算法诞生背景

各种业务场景都需要唯一ID生成的需求,特别是在高并发的情况下,在这种背景下,Twitter公司发明了雪花算法,主要目的是解决在分布式环境下,唯一ID生成的问题,得益于twitter内部牛逼的技术,雪花算法能够流传于至今并且被广泛使用。

雪花算法的优点:

  • 经测试snowflake每秒能生成26万个自增可排序的ID。
  • 分布式系统内不会产生ID碰撞(datacenter和workerId作区分)并且效率高。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高,可以根据自身业务分配bit位,非常灵活。

2.雪花ID的结构

snowflake ID 结构是一个64bit的int型数据。 image.png unused : 不使用,占1bit,为0
timestamp : 时间戳,占41bit
work id : 工作机器id,占10bit,记录工作机器的id,范围为2^10=1024,所以允许分布式最大节点数为1024个节点
sequence : 序列号,占12bit,记录同毫秒内产生的不同id,范围为2^12-1=4095,表示同一机器同一时间戳(毫秒)内产生的4095个ID序号

3.通过go使用雪花算法

3.1实现大致步骤

  1. 获取当前的毫秒时间戳;
  2. 用当前的毫秒时间戳和上次保存的时间戳进行比较;
  3. 如果和上次保存的时间戳相等,那么对序列号 sequence 加一;
  4. 如果不相等,那么直接设置 sequence 为 0 即可;
  5. 然后通过或运算拼接雪花算法需要返回的 int64 返回值。

首先我们需要定义一个 Snowflake 结构体:

type Snowflake struct {
 sync.Mutex     // 锁
 timestamp    int64 // 时间戳 ,毫秒
 workerid     int64  // 该节点的ID
 datacenterid int64 // 该节点的数据中心ID
 sequence     int64 // 序列号,1毫秒内最多生成4096个ID
}

再定义一些常数:

const (
  epoch             = int64(1577808000000)                           // 设置起始时间(时间戳/毫秒):2020-01-01 00:00:00,有效期69年
 timestampBits     = uint(41)                                       // 时间戳占用位数
 datacenteridBits  = uint(2)                                        // 数据中心id所占位数
 workeridBits      = uint(7)                                        // 机器id所占位数
 sequenceBits      = uint(12)                                       // 序列所占的位数
 timestampMax      = int64(-1 ^ (-1 << timestampBits))              // 时间戳最大值
 datacenteridMax   = int64(-1 ^ (-1 << datacenteridBits))           // 支持的最大数据中心id数量
 workeridMax       = int64(-1 ^ (-1 << workeridBits))               // 支持的最大机器id数量
 sequenceMask      = int64(-1 ^ (-1 << sequenceBits))               // 支持的最大序列id数量
 workeridShift     = sequenceBits                                   // 机器id左移位数
 datacenteridShift = sequenceBits + workeridBits                    // 数据中心id左移位数
 timestampShift    = sequenceBits + workeridBits + datacenteridBits // 时间戳左移位数
)

生成snowID

func (s *Snowflake) NextVal() int64 {
   s.Lock()
   now := time.Now().UnixNano() / 1000000 // 转毫秒
   if s.timestamp == now {
      // 当同一时间戳(精度:毫秒)下多次生成id会增加序列号
      s.sequence = (s.sequence + 1) & sequenceMask
      if s.sequence == 0 {
         // 如果当前序列超出12bit长度,则需要等待下一毫秒
         // 下一毫秒将使用sequence:0
         for now <= s.timestamp {
            now = time.Now().UnixNano() / 1000000
         }
      }
   } else {
      // 不同时间戳(精度:毫秒)下直接使用序列号:0
      s.sequence = 0
   }
   t := now - epoch
   if t > timestampMax {
      s.Unlock()
      glog.Errorf("epoch must be between 0 and %d", timestampMax-1)
      return 0
   }
   s.timestamp = now
   r := int64((t)<<timestampShift | (s.datacenterid << datacenteridShift) | (s.workerid << workeridShift) | (s.sequence))
   s.Unlock()
   return r
}

对r系列的位运算解释: 首先t表示的是现在距离epoch的时间差,我们epoch在初始化的时候设置的是2020-01-01 00:00:00,那么对于41bit的 timestamp来说会在69年之后才溢出。对t进行向左位移之后,低于timestampShift 位置上全是0 ,由datacenterid、workerid、sequence进行取或填充。