记一次雪花算法使用的坑

486 阅读2分钟

一次对雪花算法使用的踩坑记录

问题出现

公司业务使用到分表来保存主键,这时候主键就需要使用分布式自增ID,所以使用了雪花算法。 什么是雪花算法
但是在几次创建插入数据的时候,发现生成的数据主键id偶尔会有几条重复

重复id 这可把我吓坏了,不是说雪花算法是精确到毫秒级,可以支持同一节点同一毫秒生成多个ID序号,这怎么会生成重复的ID。

问题复现

赶紧看看代码有啥问题
这是最开始的代码

func main() {
    //遍历数据切片
   dataSlice := make([]string, 0)
   for _, v := range dataSlice {
   node, err := NewWorker(1)
   if err != nil {
      panic(err)
   }
   //数据主键赋值雪花生成的ID
   v.ID=node.GetId()
   //接下来是插入操作数据操作
   ……
   }
}

一开始认为的是在遍历每条数据时,生成一个新的Work节点,然后再去生成ID,应该就没问题了。但是现在出现重复了ID,唯一有可能出现的就是生成节点的问题了。然后把代码改一下,生成Work节点放到for循环外,代码如下

func main() {
   node, err := NewWorker(1)
   if err != nil {
      panic(err)
   }
    //遍历数据切片
   dataSlice := make([]string, 0)
   for _, v := range dataSlice {
   //数据主键赋值雪花生成的ID
   v.ID=node.GetId()
   //接下来是插入操作数据操作
   ……
   }
}

OK,问题解决,可是为什么会出现这种情况。(来自小白的猜测,难不成是for循环太快,一毫秒生成了重复的节点。)

问题测试

写个demo测一下,出现这种不同情况的原因是什么。按照我一开始的猜测,是和时间戳有关于是我在生成ID的时候把时间戳打印出来看一下。

const (
   workerBits  uint8 = 10
   numberBits  uint8 = 12
   workerMax   int64 = -1 ^ (-1 << workerBits)
   numberMax   int64 = -1 ^ (-1 << numberBits)
   timeShift   uint8 = workerBits + numberBits
   workerShift uint8 = numberBits
   startTime   int64 = 1525705533000 // 如果在程序跑了一段时间修改了epoch这个值 可能会导致生成相同的ID
)

type Worker struct {
   mu        sync.Mutex
   timestamp int64
   workerId  int64
   number    int64
}

func NewWorker(workerId int64) (*Worker, error) {
   if workerId < 0 || workerId > workerMax {
      return nil, errors.New("Worker ID excess of quantity")
   }
   // 生成一个新节点
   return &Worker{
      timestamp: 0,
      workerId:  workerId,
      number:    0,
   }, nil
}

func (w *Worker) GetId() (int64,  int64) {
   w.mu.Lock()
   defer w.mu.Unlock()
   now := time.Now().UnixNano() / 1e6
   if w.timestamp == now {
      w.number++
      if w.number > numberMax {
         for now <= w.timestamp {
            now = time.Now().UnixNano() / 1e6
         }
      }
   } else {
      w.number = 0
      w.timestamp = now
   }
   ID := int64((now-startTime)<<timeShift | (w.workerId << workerShift) | (w.number))
   return ID, time.Now().UnixNano() / 1e6
}
func main() {
   
   for i := 0; i < 5; i++ {
      node, err := NewWorker(1)
      if err != nil {
         panic(err)
      }
    ID, timestamp := node.GetId()
    fmt.Println("ID=", ID, "timestamp=", timestamp)
   }
}

OK,如下图所示,生成节点在for循环内时,生成的毫秒级时间戳有可能会重复,我一开始以为直接破案。 image.png 但是当我把生成节点放在for循环外时,生成的毫秒级时间戳是重复的,但是ID却是不一样的,这是怎么回事,看来并不是因为这个原因。 image.png

溯本追源

让我们来看看生成ID的函数

type Worker struct {
   mu        sync.Mutex
   timestamp int64
   workerId  int64
   number    int64
}
func (w *Worker) GetId() (int64, int64) {
   w.mu.Lock()
   defer w.mu.Unlock()
   now := time.Now().UnixNano() / 1e6
   if w.timestamp == now {
      w.number++
      if w.number > numberMax {
         for now <= w.timestamp {
            now = time.Now().UnixNano() / 1e6
         }
      }
   } else {
      w.number = 0
      w.timestamp = now
   }
   ID := int64((now-startTime)<<timeShift | (w.workerId << workerShift) | (w.number))
   return ID, time.Now().UnixNano() / 1e6
}

我们发现,在使用同一节点生成ID时,会先判断节点上次生成ID的时间戳是否和当前一致。如果不一致的话,记录当前时间戳并将number计数器清空。但是如果时间戳一致,就会将work.numer加1,再进行判断计数器有没有超过最大序列号。根据最后一行ID := int64((now-startTime)<<timeShift | (w.workerId << workerShift) | (w.number))可知,生成的ID是由当前时间减去设定的开始时间、使用的节点wokrID、以及wokrID的计数器w.number三个数据,进行按位或操作。将时间戳机器ID序列号number按位进行或运算,得到最终的唯一ID。 那么回到刚开始的问题,这次我们把W.number也打印出来看一下。
前后分别为生成节点在for循环内和for循环外的。 image.png image.png 最终破案,因为把生成节点的NewWork函数放在了for循环内,每次都会初始化节点的序列号number为0,所以生成的ID会出现重复值。而放在for循环外时,序列号number就不会每次被初始化能够正常计数,最终生成的ID就不会重复

收获与反思

  1. 通过这次Bug,去更细致的了解的雪花算法生成分布式ID的原理。
  2. 在后续编码时候,要注意这种全局使用的变量或者工具类,要在一开始就进行定义或生成,不要放在for循环里使用时再初始化