
UniqGenerator提供一個簡單、可靠、高效的、可支撐大容量和大并發的取絕對唯一ID(可以是數字型的,也可以是字元串型的)的通用機制,這裡講的“絕對”是指在同一系統内部的絕對唯一,有别于UUID(通用唯一識别碼,Universally Unique Identifier)。
在很多應用場景中有着取唯一ID的需求,比如淘寶交易單号、中國人保保單号等,它們的特點是一長串數字或字母和數字混合的長字元串,而最關鍵的一點是必須絕對唯一,1000萬中存在1個重複也不允許。
要滿足這樣的一個需求,最簡單的方法是由單獨的一台機器配置設定ID,然後供應其它機器使用,但是這種方法有兩個問題:一是整個系統對這台機器産生了強依賴;二是這台機器可能會成為瓶頸。
分析思路可歸納為兩點:一是對需求分類;二是針對需求以類舉的方式提出解決方法。
參與配置設定唯一ID的機器都需要取得一個令牌,這是它能配置設定唯一ID的先決條件。令牌是一種有限的資源,擷取令牌的方式是租約。
租期以天為機關,在一個令牌的租期未滿之前,租用它的機器獨占它,直到租期滿1天後,即假設租期為7天,則8天後其它機器都可以租用該令牌。在租期的基礎上延後1天是為保證令牌的絕對安全,防止同一個令牌在超過1台的機器上存活。
1台機器租用一個令牌後,可以對這個令牌不斷續約,續約間隔時間以小時為機關。
怎麼做到ID的唯一性?協定将根本下圖所示的這樣一個思路進行設計。
通過下圖所示的結構,即可保證産生的ID在系統内部具有絕對的唯一性(本設計方案不能保證不同系統間的ID也能絕對唯一):
針對不同需要,将結構劃分成3種類型(但可以根據需求繼續擴充):
固定長度的字元串經常被用于定義各種訂單号、交易流水号等,如中國人保(PICC)的保單号,微信的交易單号。
為滿足不同的需求,令牌和序列号兩者的字元個數是可以配置的。而日期、業務識别碼和業務自定義部分需要應用自己以參數方式傳入。
為了保證序列号的唯一性,須對序列号進行持久化記錄,以便在時間範圍内UniqGenerator程序重新開機或機器重新開機後,仍不會産生重複的序列号。
但如果僅這樣,當這個序列号的記錄檔案被删除時,則會産生問題。為降低這個風險,UniqGenerator程序在啟動時主動檢查這個檔案是否存在,如果不存在則直接啟動失敗。通過UniqGenerator的format參數可以生成這個檔案,在首次啟動時需要做一下這項工作,UniqGenerator不自動做的原因是為一定程式上保證安全性。
當需要為第一條留言或評論配置設定一個唯一的ID時,則可以使用有狀态的數字型ID,一個8位元組的無符号整數,程式處理起來也非常便利。調用程式可不關心Uniq64的内部結構,而直接将它當作整數使用。
由于隻使用了8位元組,時間部分無法精确到秒,是以序列号也需要持久化。
無狀态數字型和有狀态數字型的差別在于,無狀态的不需要持久化記錄序列号,因為它的時候精确到了秒,UniqGenerator程序每次啟動時會延遲1秒鐘,以錯過時間來保證唯一性。也是以,它比有狀态的多了4位元組,程式中不能直接當作整數使用。
#ifndef SERIAL_FILE_H
#define SERIAL_FILE_H
#include
namespace uniq_generator
{
// 運作:uniq_generator --format,格式化生成序列号檔案
// 如果uniq_generator啟動時檢測不到檔案存在,則啟動不成功。
#pragma pack(4)
// 序列号檔案頭結構
struct SerialFileHeader
uint32_t version; // 版本号
uint32_t num_blocks; // 塊數(預設100)
uint64_t padding1; // 預留
uint64_t padding2; // 預留
uint64_t padding3; // 預留
uint64_t padding4; // 預留
uint64_t padding5; // 預留
uint64_t padding6; // 預留
uint64_t padding7; // 預留
uint64_t padding8; // 預留
struct SerialBlock blocks[0];
};
// 序列号塊結構
// 對于Uniq96類型,它不需要持久化
struct SerialBlock
char name[16]; // 名稱,如果一個塊未被使用,則名稱為空,要求最後一個字元為結尾符
int64_t create_time; // 建立時間
int64_t modification_time; // 最後更新時間
union
{
uint32_t flags; // 标志字段
struct
{
uint32_t everyday_reset: 1; // 每天是否重置
uint32_t padding: 31; // 預留
};
};
uint32_t padding1; // 預留
uint64_t padding2; // 預留
uint64_t padding3; // 預留
uint64_t serial; // 序列号
uint32_t step; // 更新步大小
#pragma pack()
} // namespace uniq_generator
#endif // SERIAL_FILE_H
UniqGenerator采用弱主從分布式架構。不同于一般的主從架構,這裡的兩個Master地位均等,可同時提供讀和寫。
兩個Master間互發心跳,心跳間隔時間以秒為機關,兩者間需要做資料同步。
Agent發也往Master發心跳,心跳間隔時間以小時為機關,通過心跳的方式續約Token。
Master負責對Token的租約管理,并以心跳方式對Agent進行弱監控。
為防止Master單點的資料安全和服務可用性,需要部署兩個Master執行個體。為規避主從Master切換問題,這兩個Master地位均等,同時提供租約和續租服務。
續租可認為是讀事件,租約可認為是寫事件。對于寫事件必須得到兩個Master的共同确認,對于讀事件則隻需其中一個确認即可。
租期滿時,就需要解約,這也是一個寫事件,需要兩個Master共同确認,滿期的租約不能被續租。
需要兩個Master共同确認,是為防止資料的不一緻。一個Master重新開機後,需要先從另一Master同步資料,同步完成之前不提供服務。如果兩個Master剛好都重新開機了,則互相同步,任何一個同步完成,即可提供讀服務。
唯一ID由Agent産生,并提供多種形式的擷取接口(如HTTP取唯一ID、RPC取唯一ID等)。Agent在産生唯一ID之前,需要先從Master成功租約到一個Token,Master保證同一個Token隻會被一個Agent租用。
租期最少1天,最多可達30天,系統預設配置為7天。Master保證在租期内其它Agent不會租用到這個Token,但租期後可租給其它機器,是以Agent需要不斷的向Master續租。過租期後,則隻能重新租用新的Token。
Agent設計為單程序雙線程結構:
1) SerialThread
響應取唯一ID請求,生成唯一ID,然後傳回給請求者。
2) HeartbeatThread
專職向Master發送續約心跳,當不能正常與Master心跳時,則連接配接另一個Master,如果同任何一個Master都不能正常心跳,則輪詢重試,直到心跳正常。
在第一個版本中,Agent和Master的心跳基于Thrift RPC實作。但考慮到性能容量等因素,如果Thrift RPC不能勝任時,則可以引入基于UDP的實作。
Master是一個單程序多線程結構:
1) RPC Thread
為Agent和另一個Master提供RPC服務,實際上基于Thrift的實作,面向Agent和另一Master的RPC将是互相獨立的RPC線程。
2) Lease Thread
租約線程,負責管理租約,如對租約滿期的處理等。
3) HeartbeatThread
專職向另一個Master發心跳的線程,心跳也用于同步兩者間的資料。
Master提供白名單機制,限制隻有在白名單中的AGENT才可以申請租約,并提供一個Web界面管理租約。允許人為的強制解除租約和人工續約。
namespace cpp uniq_generator.master
// Token類型定義
// 一台機器對于同一種類型的Token,隻能租用一個
enum TokenType
TOKEN_STRING2_INCLUDE_LETTER = 2, // 2個字元,可包含A-Z字母
TOKEN_STRING3_INCLUDE_LETTER = 3, // 3個字元,可包含A-Z字母
TOKEN_STRING2_ONLY_NUMBER = 12, // 2個字元,純數字
TOKEN_STRING3_ONLY_NUMBER = 13, // 3個字元,純數字
TOKEN_UINT1 = 21, // 1個位元組的無符号整數
TOKEN_UINT2 = 22 // 2個位元組的無符号整數
}
// 租用結果
enum RentingResult
RR_SUCCESS = 0, // 租用成功
RR_RENTED = 1, // 已被其它租用
RR_UNRENTED = 2 // 未被租用
// 令牌
struct Token
1: TokenType token_type; // Token類型
2: string token; // Token
// 租約結構
struct TokenInfo
3: i64 create_time; // 租約建立時間
4: i64 modification_time; // 最近續約時間
// 面向Agent的租約服務(心跳)
service LeaseService
// 申請一個Token租約
// 成功租約到傳回非空字元串
// 租約過程中如果遇到錯誤,則抛出異常
string request_token(1: TokenType token_type);
// 續租
// tokens 被續約的Token
// 續租過程中如果遇到錯誤,則抛出異常
RentingResult rent_token(1: Token token);
// 擷取已取得的所有Tokens
// 擷取過程中如果遇到錯誤,則抛出異常
list list_tokens();
// 解約一個Token
// 解約過程中如果遇到錯誤,則抛出異常
void terminate_token(1: TokenType token_type, 2: string token);
// 心跳一下,啥都不做
void heartbeat();
UniqGenerator的實作充分利用了開源,以期大幅度提升開發效率:
1) Thrift
Agent和Master間,以及兩個Master間的網絡通訊使用的都是Thrift,使用RPC的好處是分布式程式設計變得簡單快捷,同時Thrift支援豐富的語言,使得前背景互動也變得簡單。
2) Boost
UniqGenerator使用著名的準C++标準庫Boost作為基礎類庫,以幫助提升開發效率。同時,Thrift也需要Boost。
3) gflags
由Google出品的指令行參數解析器,使用得基于指令行參數的處理變得非常簡單好用。
4) glog
由Google出品的寫日志類庫,流式的寫日志,無類型安全問題。
#ifndef UNIQ_GENERATOR_H
#define UNIQ_GENERATOR_H
// 64位的唯一數
// Uniq64的優點是可以直接做64位無符号整數使用
// 它的特點是時間精确到小時,無論是同一台機器還是不同台機器,不同小時産生的值均不會相同
struct Uniq64
uint64_t value;
uint64_t year: 7; // 目前年份減2000後的值,如2015為15,2115為115,最多支援到2127年
uint64_t month: 4; // 目前的月份
uint64_t day: 5; // 目前日期的月份天
uint64_t hour: 5; // 目前時間的小時
uint64_t token: 12; // 每台機器獨占的,用來做機器間的區分
uint64_t serial: 31; // 遞增的序列号,一天内不重複,單台機器一天最大到2147483648
std::string str() const
std::stringstream result;
result
return result.str();
}
std::string uri() const
// 96位的唯一數
// Uniq96相比Uinq64,不能直接做整數值使用
// 它的特點是時間精确到秒,無論是同一台機器還是不同台機器,不同秒産生的值均不會相同
// 使用它時,建議程序啟動時延遲一秒,以保證可以總是産生不同的值
struct Uniq96
uint64_t year: 12; // 目前年份減2000後的值,如2015為15,6096為4096,最多支援到6096年
uint64_t month: 4; // 目前日期的月份
uint64_t day: 5; // 目前日期的月份天
uint64_t hour: 5; // 目前時間的小時
uint64_t minute: 6; // 目前時間的分
uint64_t second: 6; // 目前時間的秒
uint64_t microsecond: 10; // 與目前時間的微秒數相關的值
uint64_t token: 16; // 每台機器獨占的,用來做機器間的區分
uint32_t serial; // 遞增的序列号
class CUniqException: public std::exception
public:
explicit CUniqException(int errcode, const char* errmsg, const char* file, int line) throw ()
init(errcode, errmsg, file, line);
explicit CUniqException(int errcode, const std::string& errmsg, const std::string& file, int line) throw()
init(errcode, errmsg.c_str(), file.c_str(), line);
virtual const char* what() const throw()
return _errmsg.c_str();
int errcode() const throw()
return _errcode;
const char* file() const throw()
return _file.c_str();
int line() const throw()
return _line;
private:
void init(int errcode, const char* errmsg, const char* file, int line) throw ()
_errcode = errcode;
if (errmsg != NULL)
_errmsg = errmsg;
if (file != NULL)
_file = file;
_line = line;
int _errcode;
std::string _errmsg;
std::string _file;
int _line;
//
// 工廠方法
// 取得内置的UniqGenerator
// 如果擷取過程中遇到錯誤,則抛出CUniqException
extern IUniqGenerator* get_uniq_generator() throw (CUniqException);
// 根據指定的名稱建立一個UniqGenerator
// 如果名字已存在則傳回NULL
// 如果建立過程中遇到錯誤,則抛出CUniqException
extern IUniqGenerator* create_uniq_generator(const std::string& uniq_generator_name) throw (CUniqException);
// 取唯一數接口
class IUniqGenerator
virtual ~IUniqGenerator() throw () {}
virtual std::string get_uniq(size_t token_size) throw (CUniqException) = 0;
virtual void get_uniq(Uniq64* uniq) throw (CUniqException) = 0;
virtual void get_uniq(Uniq96* uniq) throw (CUniqException) = 0;
#endif // UNIQ_GENERATOR_H