1 背景
常见的分布式系统生成唯一ID的方式由很多,主要有:
- 使用数据库,使用数据库的自增列
- 使用UUID,实现简单,但是没有排序,查询效率低
- 使用Redis自增原子性生成,但是要引入第三方组件
本文介绍一个分布式系统中高效生成唯一ID的一种方案——雪花算法
2 雪花算法
2.1概述
SnowFlake雪花算法,最早是Twitter公司再其内部用于分布式环境下生成唯一ID。再2014年开源scala语言版本。
2.2 ID组成
雪花算法的原理是生成一个64位比特位的long类型的唯一ID
由符号位、时间戳、机器ID、服务ID、序号组成:
- 固定值:占一位,固定为0,生成的ID为正数,为负数就是1
- 时间戳:占41位,以当前时间位起始值,可以使用2^41ms,大概可以使用49年
- 机器ID和服务ID:各占5位,可以部署2^10个节点
- 序号:占12位,同一毫秒,统一机器通过序号区分,可以生成2^12个不同ID
3 代码实现
本代码由golang实现:
package main
import (
"errors"
"sync"
"time"
)
const (
// 当前时间 2023-07-29 14:59:31 毫秒级别
epoch int64 = 1690613971279
// 序列号位数
numberBit int8 = 12
// 服务ID位数
serveIdBit int8 = 5
// 机器ID位数
machineIdBit int8 = 5
// 服务ID的偏移量
serveIdShift int8 = numberBit
// 机器ID的偏移量
machineIdShift int8 = numberBit + serveIdBit
// 时间戳的偏移量
timestampShift int8 = numberBit + serveIdBit + machineIdBit
// 服务ID的最大值 31
serverIdMax int64 = -1 ^ (-1 << serveIdBit)
// 机器ID的最大值 31
machineIdMax int64 = -1 ^ (-1 << machineIdBit)
// 序列号的最大值 4095
numberMax int64 = -1 ^ (-1 << numberBit)
)
type SnowFlake struct {
// 每次生产一个id都是一个原子操作
lock sync.Mutex
// 时间戳、机器ID、服务ID、序列号
timestamp int64
machineId int64
serveId int64
number int64
}
// NewSnowFlake 构造函数,传入机器ID和服务ID
func NewSnowFlake(machineId int64, serveId int64) (*SnowFlake, error) {
if machineId < 0 || machineId > machineIdMax {
return nil, errors.New("mechineId超出限制")
}
if serveId < 0 || serveId > serverIdMax {
return nil, errors.New("serveId超出限制")
}
return &SnowFlake{
timestamp: 0,
machineId: machineId,
serveId: serveId,
number: 0,
}, nil
}
func (snow *SnowFlake) NextId() int64 {
// 原子操作
snow.lock.Lock()
defer snow.lock.Unlock()
// 获取当前时间戳
now := time.Now().UnixMilli()
// 如果时间戳还是当前时间错,则序列号增加
if now == snow.timestamp {
snow.number++
// 如果超过了序列号的最大值,则更新时间戳
if snow.number > numberMax {
for now <= snow.timestamp {
now = time.Now().UnixMilli()
}
}
} else {
snow.number = 0
snow.timestamp = now
}
// 拼接最后的结果,将不同不服的数值移到指定位置
id := (snow.timestamp-epoch)<<timestampShift | (snow.machineId << machineIdShift) |
(snow.serveId << serveIdShift) | snow.number
return id
}
测试:
package main
import (
"fmt"
"testing"
)
func TestSnowFlake_NextId(t *testing.T) {
snow, _ := NewSnowFlake(10, 10)
for i := 0; i < 10; i++ {
fmt.Println(snow.NextId())
}
}
4 总结
优点:
- 不依赖第三方库或者中间件
- 算法简单,性能高,在内存中进行
- 容量大,每秒钟能生成百万ID
- 雪花算法比特位不是固定死的,可以根据业务动态的改变各部分的占位
缺点:
- 雪花算法在单机上是递增的,但是分布式情况下,可能因为节点时钟不同而不保证全局递增