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