分布式唯一ID的生成
背景:
在分布式架构下,唯一序列号生成是我们在设计一个尤其是数据库使用分库分表的时候会常见的一个问题
特性:
- 全局唯一,这是基本要求,不能出现重复
-
数字类型,趋势递增
,后面的ID必须比前面的大 -
长度短,能够提高查询效率
,这也是从MySQL数据库规范出发的,尤其是ID作为主键时 - **信息安全,**如果ID连续生成,势必会泄露业务信息,所以需要无规则不规则
-
高可用低延时,ID生成快
,能够扛住高并发,延时足够低不至于成为业务瓶颈.
雪花算法:
snowflake是推特开源的分布式ID生成算法
-
结果: long 型的ID号(64位的ID号)
-
核心思想(
生成的ID号是64位那么就对64位进行划分赋予特别的含义
):
41bit-时间戳
决定了该算法生成ID号的可用年限.
10bit-工作机器编号
决定了该分布式系统的扩容性即机器数量.
12bit-序列号
决定了每毫秒单机系统可以生成的序列号
-
拓展:
什么是时间戳?
北京时间1970年01月01日08时00分00秒到此时时刻的
总秒数
-
优势:
//实现方法:
package main
import (
"errors"
"fmt"
"sync"
"time"
)
/*
雪花算法(snowFlake)的具体实现方案:
*/
type SnowFlake struct{
mu sync.Mutex
//雪花算法开启时的起始时间戳
twepoch int64
//每一部分占用的位数
workerIdBits int64 //每个数据中心的工作机器的编号位数
datacenterIdBits int64 //数据中心的编号位数
sequenceBits int64 //每个工作机器每毫秒递增的位数
//每一部分最大的数值
maxWorkerId int64
maxDatacenterId int64
maxSequence int64
//每一部分向左移动的位数
workerIdShift int64
datacenterIdShift int64
timestampShift int64
//当前数据中心ID号
datacenterId int64
//当前机器的ID号
workerId int64
//序列号
sequence int64
//上一次生成ID号前41位的毫秒时间戳
lastTimestamp int64
}
/*
获取毫秒的时间戳
*/
func (s *SnowFlake)timeGen()int64{
return time.Now().UnixMilli()
}
/*
获取比lastTimestamp大的当前毫秒时间戳
*/
func (s *SnowFlake)tilNextMills()int64{
timeStampMill:=s.timeGen()
for timeStampMill<=s.lastTimestamp{
timeStampMill=s.timeGen()
}
return timeStampMill
}
func (s *SnowFlake)NextId()(int64,error){
s.mu.Lock()
defer s.mu.Unlock()
nowTimestamp:=s.timeGen()//获取当前的毫秒级别的时间戳
if nowTimestamp<s.lastTimestamp{
//系统时钟倒退,倒退了s.lastTimestamp-nowTimestamp
return -1,errors.New(fmt.Sprintf("clock moved backwards, Refusing to generate id for %d milliseconds",s.lastTimestamp-nowTimestamp))
}
if nowTimestamp==s.lastTimestamp{
s.sequence=(s.sequence+1)&s.maxSequence
if s.sequence==0{
//tilNextMills中有一个循环等候当前毫秒时间戳到达lastTimestamp的下一个毫秒时间戳
nowTimestamp=s.tilNextMills()
}
}else{
s.sequence=0
}
s.lastTimestamp=nowTimestamp
return (nowTimestamp-s.twepoch)<<s.timestampShift| //时间戳差值部分
s.datacenterId<<s.datacenterIdShift| //数据中心部分
s.workerId<<s.workerIdShift| //工作机器编号部分
s.sequence, //序列号部分
nil
}
func NewSnowFlake(workerId int64,datacenterId int64)(*SnowFlake,error){
mySnow:=new(SnowFlake)
mySnow.twepoch=time.Now().Unix() //返回当前时间的时间戳(时间戳是指北京时间1970年01月01日8时0分0秒到此时时刻的总秒数)
if workerId<0||datacenterId<0{
return nil,errors.New("workerId or datacenterId must not lower than 0 ")
}
/*
标准的雪花算法
*/
mySnow.workerIdBits =5
mySnow.datacenterIdBits=5
mySnow.sequenceBits=12
mySnow.maxWorkerId=-1^(-1<<mySnow.workerIdBits) //64位末尾workerIdBits位均设为1,其余设为0
mySnow.maxDatacenterId=-1^(-1<<mySnow.datacenterIdBits) //64位末尾datacenterIdBits位均设为1,其余设为0
mySnow.maxSequence=-1^(-1<<mySnow.sequenceBits) //64位末尾sequenceBits位均设为1,其余设为0
if workerId>=mySnow.maxWorkerId||datacenterId>=mySnow.maxDatacenterId{
return nil,errors.New("workerId or datacenterId must not higher than max value ")
}
mySnow.workerIdShift=mySnow.sequenceBits
mySnow.datacenterIdShift=mySnow.sequenceBits+mySnow.workerIdBits
mySnow.timestampShift=mySnow.sequenceBits+mySnow.workerIdBits+mySnow.datacenterIdBits
mySnow.lastTimestamp=-1
mySnow.workerId=workerId
mySnow.datacenterId=datacenterId
return mySnow,nil
}
func main(){
//模拟实验是生成并发400W个ID,所需要的时间
mySnow,_:=NewSnowFlake(0,0)//生成雪花算法
group:=sync.WaitGroup{}
startTime:=time.Now()
generateId:=func (s SnowFlake,requestNumber int){
for i:=0;i<requestNumber;i++{
s.NextId()
group.Done()
}
}
group.Add(4000000)
//生成并发的数为4000000
currentThreadNum:=400
for i:=0;i<currentThreadNum;i++{
generateId(*mySnow,10000)
}
group.Wait()
fmt.Printf("time: %v\n",time.Now().Sub(startTime))
}
以上分析生成400WID号只需要803.1006ms(所以单机上可以每秒生成的ID数在400W以上)
优点:
-
毫秒数在高位,自增序列在低位,
整个ID都是趋势递增
-
不依赖数据库等第三方系统,
以服务的方式部署,稳定性更高,生成的ID性能也是非常高的
- 可以根据自身业务特性分配bit位,非常灵活
缺陷:
1. 依赖机器时钟,如果**机器时钟回拨**,会导致重复ID生成.
1. 在单机上是递增的,但是由于设计到分布式环境下,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况.
如何解决单机系统中时钟回拨问题:
可以分为两种情况:
1. 如果**时间回拨时间较短,比如配置5ms以内**,那么可以直接等候一定的时间,让机器时间追上来
1. 如果**时间回拨时间较长**,我们不能接收这么长的阻塞等候,那么就有两个策略,直接拒绝,抛出异常;或者通过RD时钟回滚
布式环境下,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况.
如何解决单机系统中时钟回拨问题:
可以分为两种情况:
1. 如果**时间回拨时间较短,比如配置5ms以内**,那么可以直接等候一定的时间,让机器时间追上来
1. 如果**时间回拨时间较长**,我们不能接收这么长的阻塞等候,那么就有两个策略,直接拒绝,抛出异常;或者通过RD时钟回滚