SnowFlake算法原理介紹
在分布式系統中會將一個業務的系統部署到多台服務器上,用戶隨機訪問其中一台,而之所以引入分布式系統就是為了讓整個系統能夠承載更大的訪問量。諸如訂單號這些我們需要它是全局唯一的,同時我們基本上都會將它作為查詢條件;出於系統安全考慮不應當讓其它人輕易的就猜出我們的訂單號,同時也要防止公司的競爭對手直接通過訂單號猜測出公司業務體量;為了保證系統的快速響應那么生成算法不能太耗時。而雪花算法正好解決了這些問題。
SnowFlake 算法(雪花算法), 是Twitter開源的分布式id生成算法。其核心思想就是: 使用一個64 bit的long型的數字作為全局唯一id。它的結構如下:
下面我們來對每一部分進一步的分析:
- 符號標識位(1位):計算機中為了區分負數(1)和正數(0),設計者將第一位做為符號位,ID通常使用正數,因此最高位固定為0;
- 41位時間截(毫秒),這個是使用 當前時間 減去 開始時間 得到的值;因此一旦我們的算法投入使用,那么程序中設置的開始時間就不能再去隨意更改了,否則將可能出現重復的id值;
由於是基於時間來實現的且只有41位,由此可以計算出該算法只能使用70年左右:(2^41)/(1000*60*60*24*365) = 69.7 年
; - 10位機器ID:共計1024個節點,通常將其分為2部分:機房ID(dataCenterId) 和 機器ID(workerId);
- 12 位序列號:毫秒內的計數,共計4098個;簡單來說就是每毫秒內從0開始計算得到值;
最終SnowFlake算法總結如下:整體上按照時間自增排序,並且整個分布式系統內不會產生ID 碰撞(由機房ID和機器ID作區分),並且效率較高。最多支持1024台機器,每台機器每毫秒能夠生成最多4096個ID,整個集群理論上每秒可以生成 1024 * 1000 * 4096 = 42 億個ID。
這里不要覺得每毫秒4098個ID少了,我們計算一下每台機器理論上每秒可以支持 4096*1000 = 400萬左右;要知道天貓雙11那么大的訂單量每秒也才50萬筆;因此是完全夠用的。
算法實現
type SnowFlakeIdWorker struct {
// 開始時間戳
twepoch int64
// 機器ID所占的位數
workerIdBits int64
// 數據標識ID所占的位數
dataCenterIdBits int64
// 支持的最大機器ID
maxWorkerId int64
// 支持的最大機房 ID
maxDataCenterId int64
// 序列在ID中占的位數
sequenceBits int64
// 機器ID向左移位數
workerIdShift int64
// 機房ID向左移位數
dataCenterIdShift int64
// 時間截向左移位數
timestampLeftShift int64
// 生成序列的掩碼最大值
sequenceMask int64
// 工作機器ID
workerId int64
// 機房ID
dataCenterId int64
/**
* 毫秒內序列
*/
sequence int64
// 上次生成ID的時間戳
lastTimestamp int64
// 鎖
lock sync.Mutex
}
func (p *SnowFlakeIdWorker) init(dataCenterId int64, workerId int64) {
// 開始時間戳;這里是2021-06-01
p.twepoch = 1622476800000
// 機器ID所占的位數
p.workerIdBits = 5
// 數據標識ID所占的位數
p.dataCenterIdBits = 5
// 支持的最大機器ID,最大是31
p.maxWorkerId = -1^(-1 << p.workerIdBits)
// 支持的最大機房ID,最大是 31
p.maxDataCenterId = -1^(-1 << p.dataCenterIdBits)
// 序列在ID中占的位數
p.sequenceBits = 12
// 機器ID向左移12位
p.workerIdShift = p.sequenceBits
// 機房ID向左移17位
p.dataCenterIdShift = p.sequenceBits+p.workerIdBits
// 時間截向左移22位
p.timestampLeftShift = p.sequenceBits+p.workerIdBits+p.dataCenterIdBits
// 生成序列的掩碼最大值,最大為4095
p.sequenceMask = -1^(-1 << p.sequenceBits)
if workerId > p.maxWorkerId || workerId < 0 {
panic(errors.New(fmt.Sprintf("Worker ID can't be greater than %d or less than 0", p.maxWorkerId)))
}
if dataCenterId > p.maxDataCenterId || dataCenterId < 0 {
panic(errors.New(fmt.Sprintf("DataCenter ID can't be greater than %d or less than 0", p.maxDataCenterId)))
}
p.workerId = workerId
p.dataCenterId = dataCenterId
// 毫秒內序列(0~4095)
p.sequence = 0
// 上次生成 ID 的時間戳
p.lastTimestamp = -1
}
// 生成ID,注意此方法已經通過加鎖來保證線程安全
func (p *SnowFlakeIdWorker) nextId() int64 {
p.lock.Lock()
defer p.lock.Unlock()
timestamp := p.timeGen()
// 如果當前時間小於上一次 ID 生成的時間戳,說明發生時鍾回撥,為保證ID不重復拋出異常。
if timestamp < p.lastTimestamp {
panic(errors.New(fmt.Sprintf("Clock moved backwards. Refusing to generate id for %d milliseconds", p.lastTimestamp - timestamp)))
}
if p.lastTimestamp == timestamp {
// 同一時間生成的,則序號+1
p.sequence = (p.sequence + 1) & p.sequenceMask
// 毫秒內序列溢出:超過最大值
if p.sequence == 0 {
// 阻塞到下一個毫秒,獲得新的時間戳
timestamp = p.tilNextMillis(p.lastTimestamp)
}
} else {
// 時間戳改變,序列重置
p.sequence = 0
}
// 保存本次的時間戳
p.lastTimestamp = timestamp
// 移位並通過或運算拼到一起
return ((timestamp - p.twepoch) << p.timestampLeftShift) |
(p.dataCenterId << p.dataCenterIdShift) |
(p.workerId << p.workerIdShift) | p.sequence
}
func (p *SnowFlakeIdWorker) tilNextMillis(lastTimestamp int64) int64 {
timestamp := p.timeGen()
for ;timestamp <= lastTimestamp; {
timestamp = p.timeGen()
}
return timestamp
}
func (p *SnowFlakeIdWorker) timeGen() int64 {
return time.Now().UnixNano()/1e6
}
使用示例
idWorker := &SnowFlakeIdWorker{}
idWorker.init(0, 1)
fmt.Println(idWorker.nextId())
注意:對於一個業務來說只需要創建一個SnowFlakeIdWorker對象即可。
注意服務器不能發生時鍾回撥,即系統時間發生錯誤,因為雪花算法是基於時間來生成,所有當發生時鍾回撥后會導致出現重復ID的問題。