雪花算法生成主键ID |青训营笔记

988 阅读4分钟

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

引言

大家在做抖音项目的时候,数据库中的id是如何设计的呢?我觉得首先要考虑的就是id能存的最大值,其次就是如何保证id的唯一性,性能和趋势递增也是一个需要考虑的点。所以我们选用的是bigint类型存储+雪花算法生成。

雪花算法的核心思想就是用一个64bit的整型作为唯一id,长整型中包含毫秒级时间戳,机器编号,机器内序号。

SnowFlake(雪花算法)

全局唯一,并且有序按时间递增,同时占用空间少,生成的id仅仅是19位的整形数字,正好契合mysql的bigint数据类型,简直完美。

数据结构:

image.png

1bit-符号位 因为二进制中最高位是符号位,1表示负数,0表示正数。 生成的id一般都是用整数,所以最高位固定为0。

41bit-时间戳 用来记录时间戳,毫秒级。 41位可以表示2^(41)-1个数字, 如果只用来表示正整数(计算机中正数包含0) ,可以表示的数值范围是: 0至2^(41)-1, 减1是因为可表示的数值范围是从0开始算的,而不是1。也就是说41位可以表示2^(41)-1个毫秒的值,转化成单位年则是(2"41)-1)/(1000 "60 "60 -24 "365) =69年。

10bit-工作机器id 用来记录工作机器id.可以部署在2^(10)= 1024个节点,它包括5位datacenterld和5位。 workerld 5bit可以表示的最大正整数是245-1=31,即可以用0,1,2,3…31这32个数字,来表示不同的datecenterld或workerld。

12bit-序列号 序列号,用来记录同毫秒内产生的不同id。 12bit可以表示的最大正整数是2^(12)-1=4095,即可以用0、1.2.3. 4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。

SnowFlake可以保证:所有生成的id按时间趋势递增,整个分布式系统内不会产重复id (因为有datacenterld和workerld来做区分)

优点:

(1)高性能高可用:生成时不依赖于数据库,完全在内存中生成。

(2)容量大:每秒中能生成数百万的自增ID。

(3)ID自增:存入数据库中,索引效率高。

缺点:

依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。可能在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以忽略, 一般分布式ID只要求趋势递增,并不会严格要求递增, 90%的需求都只要求趋势递增)

Golang实现雪花算法

思路:

  1. 序列号 12位,如果同一毫秒内产生的序列号超过4096个,就等到下一毫秒再生成。同一时间的序列号从0开始递增;

  2. 机器号 10位,序列号占用12位空间,所以左移 12 位;

  3. 时间戳 41位,时间戳和0x1FFFFFFFFFF做 与运算 ,取时间戳的低 41 位,左移22位;

  4. 以上三者做 或运算 得到生成的id。

package util

import (
   "errors"
   "sync"
   "time"
)

const (
   twepoch        = int64(1483228800000)             //开始时间截 (2017-01-01)
   workeridBits   = uint(10)                         //机器id所占的位数
   sequenceBits   = uint(12)                         //序列所占的位数
   workeridMax    = int64(-1 ^ (-1 << workeridBits)) //支持的最大机器id数量
   sequenceMask   = int64(-1 ^ (-1 << sequenceBits)) //
   workeridShift  = sequenceBits                     //机器id左移位数
   timestampShift = sequenceBits + workeridBits      //时间戳左移位数
)

// A Snowflake struct holds the basic information needed for a snowflake generator worker
type Snowflake struct {
   sync.Mutex
   timestamp int64
   workerid  int64
   sequence  int64
}

// NewNode returns a new snowflake worker that can be used to generate snowflake IDs
func NewSnowflake(workerid int64) (*Snowflake, error) {

   if workerid < 0 || workerid > workeridMax {
      return nil, errors.New("workerid must be between 0 and 1023")
   }

   return &Snowflake{
      timestamp: 0,
      workerid:  workerid,
      sequence:  0,
   }, nil
}

// Generate creates and returns a unique snowflake ID
func (s *Snowflake) Generate() int64 {

   s.Lock()

   now := time.Now().UnixNano() / 1000000

   if s.timestamp == now {
      s.sequence = (s.sequence + 1) & sequenceMask

      if s.sequence == 0 {
         for now <= s.timestamp {
            now = time.Now().UnixNano() / 1000000
         }
      }
   } else {
      s.sequence = 0
   }

   s.timestamp = now

   r := int64((now-twepoch)<<timestampShift | (s.workerid << workeridShift) | (s.sequence))

   s.Unlock()
   return r
}