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_




posted @ 2021-07-17 22:54  無雙  阅读(1803)  评论(0编辑  收藏  举报