基于 Redis 的序列號(hào)服務(wù)的設(shè)計(jì)

本文講述基于 Redis序列號(hào)服務(wù)的設(shè)計(jì)诵姜,主要從序列號(hào)服務(wù)的概念需求以及服務(wù)的設(shè)計(jì)思路詳細(xì)設(shè)計(jì)等方面對(duì)其進(jìn)行闡述搏熄。

〇棚唆、前言

在筆者團(tuán)隊(duì)中,由于分布式 ID 具有單調(diào)遞增心例、形成序列的特性宵凌,我們習(xí)慣將分布式 ID 稱為序列號(hào)(Sequence),將分布式 ID 生產(chǎn)系統(tǒng)為序列號(hào)服務(wù)系統(tǒng)止后。因此瞎惫,本文以“序列號(hào)”一詞均指代分布式 ID 來進(jìn)行講述。

前些天在“開發(fā)者頭條”的熱門分享中有一篇攜程技術(shù)中心大咖寫的 《分布式架構(gòu)系統(tǒng)生成全局唯一序列號(hào)的一個(gè)思路》译株,文章中對(duì)多種分布式 ID 系統(tǒng)設(shè)計(jì)方案進(jìn)行了詳細(xì)的優(yōu)劣對(duì)比微饥,并重點(diǎn)講述了他們最終選擇以 flicker 方案為基礎(chǔ)進(jìn)行優(yōu)化改進(jìn)。另外古戴,網(wǎng)絡(luò)上闡述分布式 ID 系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)的文章數(shù)不勝數(shù),其中不少文章同樣干貨滿滿矩肩,筆者拜讀數(shù)遍现恼,受益匪淺肃续,其中包括美團(tuán)點(diǎn)評(píng) Leaf ,微信的 seqsvr叉袍。

在此始锚,筆者基于團(tuán)隊(duì)的業(yè)務(wù)適用性繼續(xù)對(duì)該主題進(jìn)行補(bǔ)充,為讀者提供一種基于 Redis 的序列號(hào)服務(wù)系統(tǒng)的設(shè)計(jì)思路(當(dāng)時(shí)主要是參考微信的 seqsvr 做的簡化方案)喳逛。

一瞧捌、服務(wù)介紹

1.1 概念

分布式 ID 生成系統(tǒng),顧名思義润文,是在分布式的架構(gòu)環(huán)境中姐呐,生成全局唯一標(biāo)識(shí)的系統(tǒng)。比如在常見的業(yè)務(wù)系統(tǒng)中典蝌,分布式 ID 可用來標(biāo)記客戶號(hào)曙砂、訂單號(hào)、文件號(hào)骏掀、優(yōu)惠券號(hào)等鸠澈,以保證這些數(shù)據(jù)的全局唯一性。正如前文所述截驮,筆者團(tuán)隊(duì)所使用的分布式 ID 具有單調(diào)遞增笑陈、形成序列的特性(既滿足全局唯一,又滿足排序的特性)葵袭,被稱為序列號(hào)涵妥,而生成序列號(hào)的系統(tǒng)被稱為序列號(hào)服務(wù)

序列號(hào)服務(wù)業(yè)內(nèi)有許多方案眶熬,比如基于 UUID妹笆,基于 Redis 的 INCR 自增,基于數(shù)據(jù)庫 ID 自增娜氏,基于 snowflake 的 bit 分段等等拳缠,它們各有優(yōu)缺點(diǎn)和適用性∶趁郑基于團(tuán)隊(duì)當(dāng)前的系統(tǒng)量級(jí)與業(yè)務(wù)適用性窟坐,筆者團(tuán)隊(duì)選擇了基于 Redis 的方案。

1.2 需求

筆者團(tuán)隊(duì)中有些業(yè)務(wù)場(chǎng)景與日切相關(guān)绵疲,所以將序列號(hào)分為兩類哲鸳,一類是遞增序列(Normal Sequence)、另一類是日切序列(Batch Sequence)盔憨。

  • 永增序列:永遠(yuǎn)按升序生成新序列號(hào)
  • 日切序列:按日切換批次的序列徙菠,同一天按升序生成新序列號(hào),換批次后序列號(hào)重置從 1 開始

二郁岩、服務(wù)設(shè)計(jì)

2.1 設(shè)計(jì)思路

我們知道婿奔,序列號(hào)服務(wù)實(shí)現(xiàn)序列號(hào)全局唯一與單調(diào)遞增可排序并非難事缺狠,它的難點(diǎn)在于如何設(shè)計(jì)好整體架構(gòu)以滿足高性能,高并發(fā)以及高可用等非功能特性萍摊。

2.1.1 Redis HINCRBY 命令

Redis 的 INCR 命令支持 Key 的 “INCR AND GET” 原子操作挤茄。利用這個(gè)特性,我們可以在 Redis 中存序列號(hào)冰木,讓分布式環(huán)境中多個(gè)取號(hào)方在集中的 Redis 中通過 INCR 命令來實(shí)現(xiàn)取號(hào)穷劈;同時(shí) Redis 是單進(jìn)程單線程的架構(gòu),不會(huì)因?yàn)槎鄠€(gè)取號(hào)方的 INCR 命令導(dǎo)致取號(hào)重復(fù)踊沸。那么基于 Redis 的 INCR 命令實(shí)現(xiàn)序列號(hào)的生產(chǎn)基本能滿足全局唯一與單調(diào)遞增的特性歇终,并且性能還不錯(cuò)。

實(shí)際上雕沿,為了存儲(chǔ)序列號(hào)的更多相關(guān)信息练湿,我們使用了 Redis 的 Hash 數(shù)據(jù)結(jié)構(gòu),Redis 同樣為 Hash 提供 HINCRBY 命令來實(shí)現(xiàn) “INCR AND GET” 原子操作审轮,詳情稍后請(qǐng)看 Redis 的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)肥哎。

2.1.2 Redis 宕機(jī)序列號(hào)恢復(fù)問題

我們?cè)傧胂耄琑edis 是內(nèi)存數(shù)據(jù)庫疾渣,在提供高性能存取的同時(shí)篡诽,在沒有開啟 RDB 或者 AOF 持久化的情況下一旦宕機(jī)序列號(hào)將會(huì)有丟失

RDB AOF 備份序列號(hào)
RDB AOF 備份序列號(hào)

即便開啟了 RDB 持久化榴捡,由于最近一次快照時(shí)間和最新一條 HINCRBY 命令的時(shí)間有可能存在時(shí)間差杈女,宕機(jī)后通過 RDB 快照恢復(fù)數(shù)據(jù)集會(huì)發(fā)生取號(hào)重復(fù)的情況;

而 AOF 持久化通過追加寫命令到 AOF 文件的方式記錄所有 Redis 服務(wù)器的寫命令吊圾,服務(wù)重啟后通過執(zhí)行這些寫命令恢復(fù)數(shù)據(jù)达椰,理論上數(shù)據(jù)集都能恢復(fù)到最新狀態(tài),不會(huì)發(fā)生取號(hào)重復(fù)的情況项乒;然而 AOF 持久化會(huì)損耗性能并且在宕機(jī)重啟后可能由于文件過大導(dǎo)致恢復(fù)數(shù)據(jù)時(shí)間過長啰劲;另外,即便能通過 AOF 重寫來壓縮文件檀何,如果是在寫 AOF 時(shí)發(fā)生宕機(jī)導(dǎo)致文件出錯(cuò)蝇裤,則需要較多時(shí)間去人為恢復(fù) AOF 文件;所以我們需要一個(gè)恢復(fù)方案來保證 Redis 序列號(hào)服務(wù)在 Redis 宕機(jī)后可快速恢復(fù)數(shù)據(jù)并且不會(huì)導(dǎo)致取號(hào)重復(fù)频鉴。

2.1.3 Redis 宕機(jī)序列號(hào)恢復(fù)方案

關(guān)系型數(shù)據(jù)庫恢復(fù)序列號(hào)
關(guān)系型數(shù)據(jù)庫恢復(fù)序列號(hào)

我們可以利用關(guān)系型數(shù)據(jù)庫來記錄一個(gè)短時(shí)內(nèi) 最大可取序列號(hào) max栓辜,取號(hào)方從 Redis 中取號(hào)時(shí)只能取小于 max 的序列號(hào)。另外垛孔,我們可以設(shè)計(jì)兩個(gè)服務(wù):一個(gè)定期地統(tǒng)計(jì)序列號(hào)消費(fèi)速度藕甩,另一個(gè)定期獲取統(tǒng)計(jì)值,當(dāng) Redis 中 當(dāng)前可取序列號(hào) cur 取號(hào)接近 max 時(shí)自動(dòng)更新 max 到一個(gè)適當(dāng)?shù)闹抵芗觯嫒霐?shù)據(jù)庫和 Redis辛萍。在 Redis 宕機(jī)的情況下悯姊,將從數(shù)據(jù)庫將最大可取序列號(hào) max 恢復(fù)成 Redis 當(dāng)前已取序列號(hào) cur,防止 Redis 取號(hào)重復(fù)贩毕。另外,也有可能關(guān)系型數(shù)據(jù)庫發(fā)生宕機(jī)仆嗦,不過由于主要的取號(hào)操作在 Redis辉阶,并且設(shè)計(jì)適當(dāng)?shù)淖畲罂扇⌒蛄刑?hào) max 能夠提供足夠時(shí)間恢復(fù)關(guān)系型數(shù)據(jù)庫

在筆者團(tuán)隊(duì)當(dāng)前的系統(tǒng)量級(jí)要求以及業(yè)務(wù)需求下瘩扼,這種設(shè)計(jì)思路經(jīng)過一段時(shí)間的生產(chǎn)實(shí)踐相對(duì)適用谆甜,接下來講述詳細(xì)的系統(tǒng)設(shè)計(jì)。

2.2 詳細(xì)設(shè)計(jì)

由于日切序列在設(shè)計(jì)上與永增序列差異不大集绰,只是多了一個(gè)日期的維度规辱,所以在詳細(xì)設(shè)計(jì)的講述過程中將以永增序列為主,日切序列不再贅述栽燕。

2.2.1 架構(gòu)圖

整體架構(gòu)圖
整體架構(gòu)圖

從上圖可知罕袋,序列號(hào)服務(wù)分兩部分:Sequence-ServerSequence-Client,這兩部分都依賴于 RedisMysql碍岔。我們先從 Redis 和 Mysql 的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)開始浴讯,然后再繼續(xù)講述 Server 和 Client 的部分。

2.2.2 Redis 數(shù)據(jù)結(jié)構(gòu)

Redis 數(shù)據(jù)結(jié)構(gòu)
Redis 數(shù)據(jù)結(jié)構(gòu)

A. Sequence Info —— 序列號(hào)相關(guān)信息

1) 數(shù)據(jù)結(jié)構(gòu)

類型 說明
type hash 哈希數(shù)據(jù)結(jié)構(gòu)
key normal:<sequenceName> 遞增序列名稱
fields cur 當(dāng)前序列號(hào)
- max 最大可用序列號(hào)
- seqs_recently 上一秒消耗的序列號(hào)數(shù)
- seqs_long_term 五分鐘內(nèi)平均每秒消耗的序列號(hào)數(shù)

取號(hào)方應(yīng)用通過 Sequence-Client 獲取序列號(hào)的時(shí)候蔼啦,通過 HINCRBY 命令增加 cur 的值并且取出榆纽,然后校驗(yàn)當(dāng)前值是否超出了最大可用序列號(hào) max。seqs_recently 和 seqs_long_term 記錄了 sequenceName 這個(gè)序列近期(上一秒)和長期(五分鐘內(nèi)平均每秒)消耗的序列號(hào)數(shù)捏肢,Sequence Server 用它來計(jì)算每次增大 max 的步長奈籽。

2) 數(shù)據(jù)樣例

序列號(hào)相關(guān)信息數(shù)據(jù)樣例
序列號(hào)相關(guān)信息數(shù)據(jù)樣例

上圖顯示最大可用序列號(hào) max為 36100,當(dāng)前已取序列號(hào) cur為 18105鸵赫,上一秒消耗的序列號(hào)數(shù) seqs_recently為 0衣屏,五分鐘內(nèi)平均每秒消耗的序列號(hào)數(shù)seqs_long_term為 0。

B. Sequence Stat —— 序列號(hào)采樣信息

1) 數(shù)據(jù)結(jié)構(gòu)

類型 說明
type String Value 字符串
key normal:<sequenceName>:stat 遞增序列名稱
value timestamp1@seqNum,...,timestamp6@seqNum 遞增序列最近N次生成序列號(hào)的時(shí)間戳奉瘤,共六個(gè)鍵值對(duì)勾拉,前五對(duì)保存長時(shí)采樣點(diǎn),第六對(duì)保存短時(shí)采樣點(diǎn)

2) 數(shù)據(jù)樣例

序列號(hào)統(tǒng)計(jì)信息數(shù)據(jù)樣例
序列號(hào)統(tǒng)計(jì)信息數(shù)據(jù)樣例

2.2.3 MySql 數(shù)據(jù)結(jié)構(gòu)

Mysql 數(shù)據(jù)結(jié)構(gòu)
Mysql 數(shù)據(jù)結(jié)構(gòu)

另外盗温,編寫數(shù)據(jù)庫自定義函數(shù) —— 更新數(shù)據(jù)庫 最大可取序列號(hào) max(其中 last_insert_id(max + step) 為了保證事務(wù))藕赞,如下:

CREATE FUNCTION `next_max_normal_sequence`(sequence_name varchar (50), step int(11))
RETURNS bigint(20)
BEGIN
    update normal_sequence set max = last_insert_id(max + step) where name = sequence_name;
    return last_insert_id();
END;

2.2.4 Sequence-Server

Sequence-Server 依賴 MySql 數(shù)據(jù)庫生成和更新 最大可取序列號(hào) max,并開啟兩個(gè)常駐線程把序列號(hào)相關(guān)信息和統(tǒng)計(jì)信息更新到 Redis卖局。

A. Sequence Transfer Thread

Sequence Transfer Thread
Sequence Transfer Thread

常駐線程 Sequence Transfer Thread 負(fù)責(zé)定時(shí)(每秒一次)通過上一秒消耗序列號(hào)數(shù) seqs_recently 和近五分鐘平均每秒消耗序列號(hào)數(shù) seqs_long_term斧蜕,預(yù)估下一秒消耗的序列號(hào)數(shù),從而預(yù)估未來十五分鐘將消耗的序列號(hào)數(shù)砚偶。如果當(dāng)前剩余序列號(hào)數(shù)不足以支撐十五分鐘批销,則計(jì)算未來三十分鐘將消耗的序列號(hào)數(shù)作為步長洒闸,更新 max 到 MySql 和 Redis,保證取號(hào)方應(yīng)用每次都能獲取到有效的序列號(hào)均芽。

B. Sequence Stat Thread

Sequence Stat Thread
Sequence Stat Thread

常駐線程 Sequence Stat Thread 負(fù)責(zé)定時(shí)(每秒一次)統(tǒng)計(jì)取號(hào)速率丘逸,以便自動(dòng)調(diào)整 Mysql 與 Redis 中的 最大可取序列號(hào) Max)。

2.2.5 Sequence-Client

Sequence-Client
Sequence-Client

Sequence-Client 以 jar 包的形式被取號(hào)方的應(yīng)用所引用掀宋,它通過封裝 “INCR AND GET深纲、校驗(yàn)序列號(hào)是否在有效范圍” 這兩個(gè)操作到 Lua 腳本中實(shí)現(xiàn)原子性以及避免多次訪問redis造成的性能消耗。

-- Sequence-Client Lua 腳本

local maxSeqNumStr = redis.pcall("HGET", KEYS[1], "max")
if type(maxSeqNumStr) == 'boolean' and maxSeqNumStr == false then
    return nil
end

local maxSeqNum = tonumber(maxSeqNumStr)
local seqNum = redis.pcall("HINCRBY", KEYS[1], "cur", 1)
if seqNum <= maxSeqNum then
    return seqNum
else
    return nil
end

三劲妙、服務(wù)總結(jié)

當(dāng)前序列號(hào)服務(wù)方案滿足:

  • 序列號(hào)全局唯一
    • 日切序列單日內(nèi)序列號(hào)全局唯一
  • 序列號(hào)單調(diào)遞增可排序
  • 高并發(fā)
  • 可用性
    • Redis(主備) + Mysql

在本機(jī)的性能測(cè)試如下:

線程數(shù) 平均響應(yīng)時(shí)間(毫秒) 吞吐量(個(gè)/秒)
1 4 233
10 5 1989
100 16 5923
500 19 5505

總的來講湃鹊,當(dāng)前我們?cè)O(shè)計(jì)的序列號(hào)服務(wù)依舊能適用于業(yè)務(wù)需要。隨著系統(tǒng)量級(jí)的增大以及業(yè)務(wù)需求的變更與演進(jìn)镣奋,序列號(hào)服務(wù)也會(huì)隨之做出調(diào)整币呵。比如 Redis 性能可能成為瓶頸,那么可以在 sequence client 的 HINCRBY 命令上增加大于 1 的增量侨颈,提供批量獲取序列號(hào)的功能(需要調(diào)整統(tǒng)計(jì)序列號(hào)消費(fèi)速率來協(xié)助自動(dòng)調(diào)整 max 值)余赢;也可以為取號(hào)方提供 Redis 的分片功能,不同的取號(hào)方在各自 Redis 中取序列號(hào)等等肛搬。

至此本文結(jié)束没佑,希望可以為讀者提供一種基于 Redis 的序列號(hào)服務(wù)系統(tǒng)的設(shè)計(jì)思路。當(dāng)然温赔,其中方案的優(yōu)缺點(diǎn)以及改進(jìn)點(diǎn)蛤奢,讀者亦可自行思考總結(jié),找到適用于自己的方案陶贼。

轉(zhuǎn)載聲明:未經(jīng)授權(quán)不得轉(zhuǎn)載啤贩,授權(quán)后轉(zhuǎn)載請(qǐng)注明出處并附上原文鏈接。


更多閱讀請(qǐng)關(guān)注【泛金融技術(shù)】微信公眾號(hào)拜秧。

簡歷投遞
簡歷投遞
微信公眾號(hào)
微信公眾號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痹屹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子枉氮,更是在濱河造成了極大的恐慌志衍,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聊替,死亡現(xiàn)場(chǎng)離奇詭異楼肪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)惹悄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門春叫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事暂殖〖劢常” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵呛每,是天一觀的道長踩窖。 經(jīng)常有香客問我,道長晨横,這世上最難降的妖魔是什么毙石? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮颓遏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滞时。我一直安慰自己叁幢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布坪稽。 她就那樣靜靜地躺著曼玩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窒百。 梳的紋絲不亂的頭發(fā)上黍判,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音篙梢,去河邊找鬼顷帖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛渤滞,可吹牛的內(nèi)容都是我干的贬墩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼妄呕,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼陶舞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绪励,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤肿孵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后疏魏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體停做,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年蠢护,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雅宾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖眉抬,靈堂內(nèi)的尸體忽然破棺而出贯吓,到底是詐尸還是另有隱情,我是刑警寧澤蜀变,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布悄谐,位于F島的核電站,受9級(jí)特大地震影響库北,放射性物質(zhì)發(fā)生泄漏爬舰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一寒瓦、第九天 我趴在偏房一處隱蔽的房頂上張望情屹。 院中可真熱鬧,春花似錦杂腰、人聲如沸垃你。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惜颇。三九已至,卻和暖如春少辣,著一層夾襖步出監(jiān)牢的瞬間凌摄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工漓帅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锨亏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓煎殷,卻偏偏與公主長得像屯伞,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子豪直,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容