天天看點

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

UniqGenerator提供一個簡單、可靠、高效的、可支撐大容量和大并發的取絕對唯一ID(可以是數字型的,也可以是字元串型的)的通用機制,這裡講的“絕對”是指在同一系統内部的絕對唯一,有别于UUID(通用唯一識别碼,Universally Unique Identifier)。

在很多應用場景中有着取唯一ID的需求,比如淘寶交易單号、中國人保保單号等,它們的特點是一長串數字或字母和數字混合的長字元串,而最關鍵的一點是必須絕對唯一,1000萬中存在1個重複也不允許。

要滿足這樣的一個需求,最簡單的方法是由單獨的一台機器配置設定ID,然後供應其它機器使用,但是這種方法有兩個問題:一是整個系統對這台機器産生了強依賴;二是這台機器可能會成為瓶頸。

分析思路可歸納為兩點:一是對需求分類;二是針對需求以類舉的方式提出解決方法。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

參與配置設定唯一ID的機器都需要取得一個令牌,這是它能配置設定唯一ID的先決條件。令牌是一種有限的資源,擷取令牌的方式是租約。

租期以天為機關,在一個令牌的租期未滿之前,租用它的機器獨占它,直到租期滿1天後,即假設租期為7天,則8天後其它機器都可以租用該令牌。在租期的基礎上延後1天是為保證令牌的絕對安全,防止同一個令牌在超過1台的機器上存活。

1台機器租用一個令牌後,可以對這個令牌不斷續約,續約間隔時間以小時為機關。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

怎麼做到ID的唯一性?協定将根本下圖所示的這樣一個思路進行設計。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

通過下圖所示的結構,即可保證産生的ID在系統内部具有絕對的唯一性(本設計方案不能保證不同系統間的ID也能絕對唯一):

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

針對不同需要,将結構劃分成3種類型(但可以根據需求繼續擴充):

固定長度的字元串經常被用于定義各種訂單号、交易流水号等,如中國人保(PICC)的保單号,微信的交易單号。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

為滿足不同的需求,令牌和序列号兩者的字元個數是可以配置的。而日期、業務識别碼和業務自定義部分需要應用自己以參數方式傳入。

為了保證序列号的唯一性,須對序列号進行持久化記錄,以便在時間範圍内UniqGenerator程序重新開機或機器重新開機後,仍不會産生重複的序列号。

但如果僅這樣,當這個序列号的記錄檔案被删除時,則會産生問題。為降低這個風險,UniqGenerator程序在啟動時主動檢查這個檔案是否存在,如果不存在則直接啟動失敗。通過UniqGenerator的format參數可以生成這個檔案,在首次啟動時需要做一下這項工作,UniqGenerator不自動做的原因是為一定程式上保證安全性。

當需要為第一條留言或評論配置設定一個唯一的ID時,則可以使用有狀态的數字型ID,一個8位元組的無符号整數,程式處理起來也非常便利。調用程式可不關心Uniq64的内部結構,而直接将它當作整數使用。

由于隻使用了8位元組,時間部分無法精确到秒,是以序列号也需要持久化。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

無狀态數字型和有狀态數字型的差別在于,無狀态的不需要持久化記錄序列号,因為它的時候精确到了秒,UniqGenerator程序每次啟動時會延遲1秒鐘,以錯過時間來保證唯一性。也是以,它比有狀态的多了4位元組,程式中不能直接當作整數使用。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口
UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

#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。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

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都不能正常心跳,則輪詢重試,直到心跳正常。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

在第一個版本中,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發心跳的線程,心跳也用于同步兩者間的資料。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

Master提供白名單機制,限制隻有在白名單中的AGENT才可以申請租約,并提供一個Web界面管理租約。允許人為的強制解除租約和人工續約。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

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出品的寫日志類庫,流式的寫日志,無類型安全問題。

UniqGenerator - 生成唯一ID技術方案 1. 前言 2. 分析思路 3. 協定結構 4. 序列号持久化 5. 系統架構 6. 實作結構 7. 程式設計接口

#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