C++實現雪花算法(處理時間回跳)


toc

雪花算法介紹

雪花算法是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_





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM