雪花算法介紹
雪花算法是Twitter開源的唯一ID生成算法。ID的有效部分有三個:
- 41位時間戳部分:此部分是雪花算法的關鍵部分,因為時間是唯一且單調遞增的,以時間作為關鍵部分,理論上ID便不會重復(但計算機上的時間計量卻可能不是唯一且單調遞增的,存在時間回跳或前跳現象),時間戳精度為毫秒
- 10位機器ID部分:此部分唯一后,允許分布式環境中每個節點生成的ID唯一
- 12位序列號部分:此部分允許同一節點同一毫秒生成多個ID(通過遞增實現唯一),相當於通過編號的形式,把時間戳粒度再次細分
結合上面的文字描述,看下下面的圖,會更好理解
/*-------高位---------------------------------------------共64位---------------------------------------------低位-------|
|-----------------------------------------------------------------------------------------------------------------------|
| 0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000 |
|未使用 | 41位時間戳 | 5位DataCenterID | 5位WorkerID | 12位序列號 |
|-----------------------------------------------------------------------------------------------------------------------|
|未使用 | 41位時間戳 | 10位機器ID | 12位序列號 |
|----------------------------------------------------------------------------------------------------------------------*/
單從設計上看,雪花算法理論上存在如下特點:
- ID單調遞增
- 系統內全局唯一(前提是每個節點機器ID唯一)
- 內含時間戳,可計算ID生成時間
- ID非常緊湊,僅有64位
- 時間戳部分可容納(2 ^ 41) / (1000 * 60 * 60* 24 * 365) = 69.7年
- 可支持2 ^ 10 = 1024個節點
- 每個節點一毫秒內最大能產生2 ^ 12 = 4096個ID
- 機器性能允許情況下,每秒可生成4096 * 1000 = 4096000個ID
在實際使用時,會發現雪花算法格外依賴計算機系統時間,一旦系統時間回退,將會導致重復ID的出現
帶時間回退處理實現一
雪花算法ID生成源碼在這里
根據對算法的理解,我實現了自己的C++版本,增加了時間回退處理、單例、按需加鎖,代碼如下:
#ifndef __IDGENERATER_H_
#define __IDGENERATER_H_
#include <mutex>
#include <string>
#include <chrono>
#include <thread>
#include <cstdint>
#include <stdexcept>
/*
Twitter雪花算法
*/
/*-------高位---------------------------------------------共64位---------------------------------------------低位-------|
|-----------------------------------------------------------------------------------------------------------------------|
| 0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000 |
|未使用 | 41位時間戳 | 5位DataCenterID | 5位WorkerID | 12位序列號 |
|-----------------------------------------------------------------------------------------------------------------------|
|未使用 | 41位時間戳 | 10位機器ID | 12位序列號 |
|----------------------------------------------------------------------------------------------------------------------*/
//各部分所占大小
constexpr int SEQUENCE_BITS = 12; //12位序列號,毫秒內計數,一個機器上1毫秒內最多能產生4096個ID
constexpr int WORKER_ID_BITS = 5;
constexpr int DATA_CENTER_ID_BITS = 5; //5位DataCenterID與5位WorkerID合在一起,是機器ID,共10位,最大能支持1024個節點
constexpr int TIMESTAMP_BITS = 41; //41位時間戳,能容納69.7年 ==> (2 ^ 41) / (1000 * 60 * 60* 24 * 365) = 69.7
//各部分偏移 根據前一部分所占位寬度決定后一部分偏移量
constexpr int SEQUENCE_ID_SHIFT = 0;
constexpr int WORK_ID_SHIFT = SEQUENCE_BITS;
constexpr int DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
constexpr int TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
//DataCenterID與WorkerID最大取值 根據所占位寬度計算最大值
constexpr std::int64_t MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1;
constexpr std::int64_t MAX_DATA_CENTER_ID = (1 << DATA_CENTER_ID_BITS) - 1;
//序列號掩碼 用於控制序列號取值范圍
constexpr std::int64_t SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
//時間起點 時間戳雖然能容納69.7年,但如果直接存Unix時間戳(1970.1.1),最大只能支持到2039年
// 所以添加一個比Unix紀元時間晚的開始時間,存相對於開始時間的偏移,那么支持的最大時間則為開始時間 + 69.7年
constexpr std::int64_t START_POINT = 1625068800000LL; //2021.7.1 00:00:00 ==> 41位時間支持到 2090年
//無鎖類 符合基本可鎖定要求,但是不添加鎖操作
class NonLockType{
public:
constexpr void lock(){
}
constexpr void unlock(){
}
};
template<typename LockType = NonLockType>
class IDGenerater final{
public:
static IDGenerater *GetInstance(int iDataCenterID = 0, int iWorkerID = 0){
if(iDataCenterID < 0 || MAX_DATA_CENTER_ID < iDataCenterID){
throw std::invalid_argument(std::string("iDataCenterID不應小於0或大於") + std::to_string(MAX_DATA_CENTER_ID));
}
if(iWorkerID < 0 || MAX_WORKER_ID < iWorkerID){
throw std::invalid_argument(std::string("iWorkerID不應小於0或大於") + std::to_string(MAX_WORKER_ID));
}
static IDGenerater GeneraterInstance(iDataCenterID, iWorkerID); //magic static C++11后靜態局部變量初始化已經是線程安全的
return &GeneraterInstance;
}
std::int64_t NextID(){
std::lock_guard<LockType> lock(m_lock);
auto i64CurTimeStamp = GetCurrentTimeStamp();
if(i64CurTimeStamp < m_i64LastTimeStamp){ //時間回退,睡眠到下一個毫秒再生成
i64CurTimeStamp = GetNextTimeStampBySleep();
} else if(i64CurTimeStamp == m_i64LastTimeStamp){ //一毫秒內生成多個ID
m_i64SequenceID = (m_i64SequenceID + 1) & SEQUENCE_MASK; //更新序列號
if(0 == m_i64SequenceID){ //達到該毫秒能生成的最大ID數量,循環到下一個毫秒再生成
i64CurTimeStamp = GetNextTimeStampByLoop(i64CurTimeStamp);
}
} else{ //新時間,序列號從頭開始
m_i64SequenceID = 0;
}
m_i64LastTimeStamp = i64CurTimeStamp;
return ((i64CurTimeStamp - START_POINT) << TIMESTAMP_SHIFT)
| (m_i64DataCenterID << DATA_CENTER_ID_SHIFT)
| (m_i64WorkerID << WORK_ID_SHIFT)
| (m_i64SequenceID << SEQUENCE_ID_SHIFT);
}
private:
std::int64_t GetCurrentTimeStamp(){
auto tpTimePoint = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now()); //獲取時間並降低精度到毫秒
return tpTimePoint.time_since_epoch().count(); //得到時間戳
}
std::int64_t GetNextTimeStampByLoop(std::int64_t i64CurTimeStamp){
while(i64CurTimeStamp <= m_i64LastTimeStamp)
{
i64CurTimeStamp = GetCurrentTimeStamp();
}
return i64CurTimeStamp;
}
std::int64_t GetNextTimeStampBySleep(){
auto dDuration = std::chrono::milliseconds(m_i64LastTimeStamp); //時間紀元到現在經歷的時間段
auto tpTime = std::chrono::time_point<std::chrono::system_clock, std::chrono::milliseconds>(dDuration); //得到時間點
std::this_thread::sleep_until(tpTime);
return GetCurrentTimeStamp();
}
private:
IDGenerater(int iDataCenterID, int iWorkerID) :m_i64DataCenterID(iDataCenterID), m_i64WorkerID(iWorkerID), m_i64SequenceID(0), m_i64LastTimeStamp(0){
}
IDGenerater() = delete;
~IDGenerater() = default;
IDGenerater(const IDGenerater& rhs) = delete;
IDGenerater(IDGenerater&& rhs) = delete;
IDGenerater& operator=(const IDGenerater& rhs) = delete;
IDGenerater& operator=(IDGenerater&& rhs) = delete;
private:
std::int64_t m_i64DataCenterID;
std::int64_t m_i64WorkerID;
std::int64_t m_i64SequenceID;
std::int64_t m_i64LastTimeStamp;
LockType m_lock;
};
using NonLockIDGenerater = IDGenerater<>;
#endif //!__IDGENERATER_H_
- 使用單例模式處理ID生成器類,保證生成器全局唯一,每個線程拿到的生成器均是同一個
- 在C++11中,靜態局部變量的初始化是線程安全,且僅初始化僅會被調用一次,非常適合實現懶漢單例
- 使用帶默認參數的類模板,搭配空鎖定/解鎖實現的鎖,在實例化時可根據需求選擇是否使用鎖,以及使用何種鎖
- 如果多個線程均需生成ID,實例化生成器類模板時,必須傳入非空實現鎖類型,使NextID操作互斥,否則可能產生重復ID
- 如果僅有一個線程生成ID,實例化生成器類模板時,建議使用模板默認參數,以得到最佳性能
- 通過sleep的方式勉強處理了時間回退,雖然處理了ID重復問題,但影響了可用性與ID生成效率,這種解決辦法存在缺陷
帶時間回退處理實現二
steady_clock介紹
C++11引入了單調時鍾std::chrono::steady_clock,他與操作系統時鍾無關,機器開機狀態下不會回退,一般用作間隔時間計算。
但從steady_clock獲取到的時間卻不是當前時間,而是開機到現在經過的時間, 也就是操作系統運行時間,這就導致一旦重新開機,steady_clock時間又將重0開始
可以用以下代碼片段,對比系統平台運行時間(Linux下命令cat /proc/uptime Windows下任務管理器),來驗證steady_clock時間
auto tpTime = std::chrono::steady_clock::now(); //獲取當前時間time_point
auto tpTimePoint = std::chrono::time_point_cast<std::chrono::milliseconds>(tpTime); //降低精度到毫秒
auto dDuration = tpTimePoint.time_since_epoch(); //返回時間紀元到現在經歷的時間段
auto tsTimeStamp = dDuration.count(); //得到時間戳
處理時間回退
可以利用steady_clock開機時間戳不單調遞增且不回退的特性,想辦法處理它重啟時間置0帶來的影響
根據IDGenerater初始化時刻system_clock時間往前推算啟動時間(推算出的時間可能不准確,依賴於初始化時刻system_clock是否准確),每次計算時間戳時,以啟動時間+運行時間作為當前時間戳,便可以得到一個回退概率更低的時間戳
- 如果單用steady_clock,每次重啟后生成的ID必然重復
- 如果單用system_clock,無法處理進程重啟情況下,時間回退導致的ID重復
- 如果steady_clock配合system_clock,除非重啟系統,並控制IDGenerater實例化時系統運行時間、以及實例化時的系統時間,使本次計算當前時間戳小於、等於上次開機的當前時間戳,才會生成重復ID
steady_clock與system_clock配合后,雪花算法仍然滿足上述8個理論特點,但降低了對系統時間的依賴,同時避免了時間回退時,sleep或while生成不了ID的尷尬
#ifndef __IDGENERATER_H_
#define __IDGENERATER_H_
#include <mutex>
#include <string>
#include <chrono>
#include <thread>
#include <cstdint>
#include <stdexcept>
/*
Twitter雪花算法
*/
/*-------高位---------------------------------------------共64位---------------------------------------------低位-------|
|-----------------------------------------------------------------------------------------------------------------------|
| 0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000 |
|未使用 | 41位時間戳 | 5位DataCenterID | 5位WorkerID | 12位序列號 |
|-----------------------------------------------------------------------------------------------------------------------|
|未使用 | 41位時間戳 | 10位機器ID | 12位序列號 |
|----------------------------------------------------------------------------------------------------------------------*/
//各部分所占大小
constexpr int SEQUENCE_BITS = 12; //12位序列號,毫秒內計數,一個機器上1毫秒內最多能產生4096個ID
constexpr int WORKER_ID_BITS = 5;
constexpr int DATA_CENTER_ID_BITS = 5; //5位DataCenterID與5位WorkerID合在一起,是機器ID,共10位,最大能支持1024個節點
constexpr int TIMESTAMP_BITS = 41; //41位時間戳,能容納69.7年 ==> (2 ^ 41) / (1000 * 60 * 60* 24 * 365) = 69.7
//各部分偏移 根據前一部分所占位寬度決定后一部分偏移量
constexpr int SEQUENCE_ID_SHIFT = 0;
constexpr int WORK_ID_SHIFT = SEQUENCE_BITS;
constexpr int DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
constexpr int TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
//DataCenterID與WorkerID最大取值 根據所占位寬度計算最大值
constexpr std::int64_t MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1;
constexpr std::int64_t MAX_DATA_CENTER_ID = (1 << DATA_CENTER_ID_BITS) - 1;
//序列號掩碼 用於控制序列號取值范圍
constexpr std::int64_t SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
//時間起點 時間戳雖然能容納69.7年,但如果直接存Unix時間戳(1970.1.1),最大只能支持到2039年
// 所以添加一個比Unix紀元時間晚的開始時間,存相對於開始時間的偏移,那么支持的最大時間則為開始時間 + 69.7年
constexpr std::int64_t START_POINT = 1625068800000LL; //2021.7.1 00:00:00 ==> 41位時間支持到 2090年
//無鎖類 符合基本可鎖定要求,但是不添加鎖操作
class NonLockType{
public:
constexpr void lock(){
}
constexpr void unlock(){
}
};
template<typename LockType = NonLockType>
class IDGenerater final{
public:
static IDGenerater *GetInstance(int iDataCenterID = 0, int iWorkerID = 0){
if(iDataCenterID < 0 || MAX_DATA_CENTER_ID < iDataCenterID){
throw std::invalid_argument(std::string("iDataCenterID不應小於0或大於") + std::to_string(MAX_DATA_CENTER_ID));
}
if(iWorkerID < 0 || MAX_WORKER_ID < iWorkerID){
throw std::invalid_argument(std::string("iWorkerID不應小於0或大於") + std::to_string(MAX_WORKER_ID));
}
static IDGenerater GeneraterInstance(iDataCenterID, iWorkerID); //magic static C++11后靜態局部變量初始化已經是線程安全的
return &GeneraterInstance;
}
std::int64_t NextID(){
std::lock_guard<LockType> lock(m_lock);
auto i64CurTimeStamp = m_i64BootTimeStamp + GetCurrentTimeStamp<std::chrono::steady_clock>(); //以m_i64BootTimeStamp單調遞增時間
if(i64CurTimeStamp < m_i64LastTimeStamp){ //時間回退,睡眠到下一個毫秒再生成
i64CurTimeStamp = GetNextTimeStampBySleep();
} else if(i64CurTimeStamp == m_i64LastTimeStamp){ //一毫秒內生成多個ID
m_i64SequenceID = (m_i64SequenceID + 1) & SEQUENCE_MASK; //更新序列號
if(0 == m_i64SequenceID){ //達到該毫秒能生成的最大ID數量,循環到下一個毫秒再生成
i64CurTimeStamp = GetNextTimeStampByLoop(i64CurTimeStamp);
}
} else{ //新時間,序列號從頭開始
m_i64SequenceID = 0;
}
m_i64LastTimeStamp = i64CurTimeStamp;
return ((i64CurTimeStamp - START_POINT) << TIMESTAMP_SHIFT)
| (m_i64DataCenterID << DATA_CENTER_ID_SHIFT)
| (m_i64WorkerID << WORK_ID_SHIFT)
| (m_i64SequenceID << SEQUENCE_ID_SHIFT);
}
private:
template<typename ClockType>
std::int64_t GetCurrentTimeStamp(){
auto tpTimePoint = std::chrono::time_point_cast<std::chrono::milliseconds>(ClockType::now()); //獲取時間並降低精度到毫秒
return tpTimePoint.time_since_epoch().count(); //得到時間戳
}
std::int64_t GetNextTimeStampByLoop(std::int64_t i64CurTimeStamp){
while(i64CurTimeStamp <= m_i64LastTimeStamp)
{
i64CurTimeStamp = m_i64BootTimeStamp + GetCurrentTimeStamp<std::chrono::steady_clock>();
}
return i64CurTimeStamp;
}
std::int64_t GetNextTimeStampBySleep(){
auto dDuration = std::chrono::milliseconds(m_i64LastTimeStamp); //時間紀元到現在經歷的時間段
auto tpTime = std::chrono::time_point<std::chrono::system_clock, std::chrono::milliseconds>(dDuration); //得到時間點
std::this_thread::sleep_until(tpTime);
return m_i64BootTimeStamp + GetCurrentTimeStamp<std::chrono::steady_clock>();
}
private:
IDGenerater(int iDataCenterID, int iWorkerID) :m_i64DataCenterID(iDataCenterID), m_i64WorkerID(iWorkerID), m_i64SequenceID(0), m_i64LastTimeStamp(0){
m_i64BootTimeStamp = GetCurrentTimeStamp<std::chrono::system_clock>() - GetCurrentTimeStamp<std::chrono::steady_clock>(); //系統開機時間(可能有誤,取決於實例化時系統時間)
}
IDGenerater() = delete;
~IDGenerater() = default;
IDGenerater(const IDGenerater& rhs) = delete;
IDGenerater(IDGenerater&& rhs) = delete;
IDGenerater& operator=(const IDGenerater& rhs) = delete;
IDGenerater& operator=(IDGenerater&& rhs) = delete;
private:
std::int64_t m_i64DataCenterID;
std::int64_t m_i64WorkerID;
std::int64_t m_i64SequenceID;
std::int64_t m_i64LastTimeStamp;
std::int64_t m_i64BootTimeStamp;
LockType m_lock;
};
using NonLockIDGenerater = IDGenerater<>;
#endif //!__IDGENERATER_H_
