一次对雪花算法使用的踩坑记录
问题出现
公司业务使用到分表来保存主键,这时候主键就需要使用分布式自增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循环内时,生成的毫秒级时间戳有可能会重复,我一开始以为直接破案。
但是当我把生成节点放在for循环外时,生成的毫秒级时间戳是重复的,但是ID却是不一样的,这是怎么回事,看来并不是因为这个原因。
溯本追源
让我们来看看生成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循环外的。
最终破案,因为把生成节点的NewWork函数放在了for循环内,每次都会初始化节点的序列号number为0,所以生成的ID会出现重复值。而放在for循环外时,序列号number就不会每次被初始化能够正常计数,最终生成的ID就不会重复
收获与反思
- 通过这次Bug,去更细致的了解的雪花算法生成分布式ID的原理。
- 在后续编码时候,要注意这种全局使用的变量或者工具类,要在一开始就进行定义或生成,不要放在for循环里使用时再初始化