node-ratelimiter

github地址:傳送門

一、簡介

這是一個(gè)nodejs版本的接口頻率算法----令牌桶算法缠俺。在P時(shí)間段里,只能被調(diào)用N次。這段時(shí)間過后薪贫,又重新有了N次機(jī)會(huì)。(這個(gè)算法有點(diǎn)不是很完美丙者,因?yàn)槟茉跇O短的時(shí)間內(nèi)哥纫,發(fā)起2N次請(qǐng)求,可能會(huì)給服務(wù)器帶來一定的壓力)

二召川、源碼分析

這里的分析南缓,以函數(shù)為一個(gè)小的基本單元來進(jìn)行。

var assert=require('assert');

這里引入的是nodejs的斷言模塊荧呐,當(dāng)不符合預(yù)期的時(shí)候汉形,會(huì)拋出異常。


function Limiter(opts) {

this.id=opts.id;? ? ? // 唯一標(biāo)識(shí)倍阐,如用戶id

this.db=opts.db; ? ?// redis數(shù)據(jù)庫實(shí)例

assert(this.id,'.id required');

assert(this.db,'.db required');

this.max=opts.max||2500; ? ?// 默認(rèn)可調(diào)用次數(shù)(N)

this.duration=opts.duration||3600000; ? ?// 默認(rèn)間隔時(shí)間(P概疆,一小時(shí))

this.prefix='limit:'+this.id+':'; ? ?// redis的key

}

上面是一個(gè)Limiter類,在初始化的時(shí)候傳入一系列的配置峰搪。


Limiter.prototype.inspect=function() {

return'

+this.id+', duration='

+this.duration+', max='

+this.max+'>';

};

這個(gè)方法岔冀,方便效果的展示


// 判斷第一個(gè)值是不是為空(這里指的是key: "limit:<id>:count"對(duì)應(yīng)的值),如果不存在的話概耻,表示redis沒有這個(gè)記錄使套,需要重新分配次數(shù)和時(shí)間給當(dāng)前用戶

function isFirstReplyNull(replies) {

? ? ? ? if (!replies) {

? ? ? ? ? ? ? ? return true;

? ? ? ? ? }

? ? ? ? ? return Array.isArray(replies[0]) ?

? ? ? ? ? ? ? ? ? ?// ioredis

? ? ? ? ? ? ? ? ? ?!replies[0][1] :

? ? ? ? ? ? ? ? ? ? // node_redis

? ? ? ? ? ? ? ? ? ? ? !replies[0];

}


// 這個(gè)是核心方法

Limiter.prototype.get = function (fn) {

var count = this.prefix + 'count'; ? ?// 剩余次數(shù)

var limit = this.prefix + 'limit'; ? ? ?// 最多次數(shù)

var reset = this.prefix + 'reset'; ? ?// 失效時(shí)間

var duration = this.duration; ? ? ?// 間隔時(shí)間

var max = this.max;

var db = this.db;

function create() {

? ? ? // 為當(dāng)前用戶開辟一塊新的內(nèi)存,保存調(diào)用情況咐蚯⊥觯總共有三個(gè)key值,分別為上面的count春锋、limit矫膨、reset

}

function decr(res) {

? ? // 收到用戶的請(qǐng)求,進(jìn)行計(jì)算,如果允許訪問侧馅,則減少一次機(jī)會(huì)危尿,否則直接返回

}

function mget() {

? ? // 調(diào)用這個(gè)方法直接,redis中肯定會(huì)存有該用戶相關(guān)情況馁痴,如果不存在的話谊娇,就調(diào)用create方法;存在的話罗晕,調(diào)用decr方法济欢,在庫存中減去一次。

}

mget();

};


下面分開來講解上面提到的三個(gè)方法小渊。

mget();

function mget() {

? ? ? db.watch([count],function(err) {

? ? ? ? ? ? ? if(err)returnfn(err);

? ? ? ? ? ? ? db.mget([count, limit, reset],function(err,res) {

? ? ? ? ? ? ? ? ? ? ?if(err) return fn(err);

? ? ? ? ? ? ? ? ? ? ?if(!res[0]&&res[0]!==0) return create();

? ? ? ? ? ? ? ? ? ? decr(res);

? ? ? ? ? ? ?});

? ? ? ?});

}

上面用到一個(gè) watch 命令法褥。

WATCH命令可以監(jiān)控一個(gè)或多個(gè)鍵,一旦其中有一個(gè)鍵被修改(或刪除)酬屉,之后的事務(wù)就不會(huì)執(zhí)行半等。監(jiān)控一直持續(xù)到EXEC命令(事務(wù)中的命令是在EXEC之后才執(zhí)行的,所以在MULTI命令后可以修改WATCH監(jiān)控的鍵值)

在create方法和decr方法里面呐萨,都會(huì)使用multi和exec命令杀饵。我們要確保兩個(gè)方法不能同時(shí)修改count值,所以谬擦,我們需要加上這個(gè)指令切距。

如果沒有分配內(nèi)存,就調(diào)用create方法進(jìn)行分配怯屉,否則就直接調(diào)用方法decr去庫存蔚舀。


create()

function create() {

? ? ? var ex = (Date.now() + duration) / 1000 | 0; ? // 失效時(shí)間

? ? ? db.multi()

? ? ? .set([count, max, 'PX', duration, 'NX'])

? ? ? ?.set([limit, max, 'PX', duration, 'NX'])

? ? ? ?.set([reset, ex, 'PX', duration, 'NX'])

? ? ? ?.exec(function (err, res) {

? ? ? ? ? ? ?if (err) return fn(err);

? ? ? ? ? ?// If the request has failed, it means the values already

? ? ? ? ? ?// exist in which case we need to get the latest values.

? ? ? ? ? ?if (isFirstReplyNull(res)) return mget();

? ? ? ? ? ? fn(null, {

? ? ? ? ? ? ? ? total: max,

? ? ? ? ? ? ? ?remaining: max,

? ? ? ? ? ? ? ? reset: ex

? ? ? ? ? });

? ? ?});

}

上面這個(gè)方法也很好理解饵沧。首先計(jì)算失效時(shí)間ex锨络,然后依次往這三個(gè)key賦值。如果恰好碰到內(nèi)存不見了(這三個(gè)key沒有了狼牺,至于為什么會(huì)沒有羡儿,也許是redis不小心被清空了,反正就是突然沒了)是钥,就調(diào)用mget方法(等于是重新跑一次這個(gè)流程)掠归。否則,就返回分配好的內(nèi)存悄泥,告訴調(diào)用者最大次數(shù)total虏冻,剩余次數(shù)remaining, 失效時(shí)間reset弹囚。


decr()

function decr(res) {

? ? var n=~~res[0]; ? ?// 剩余次數(shù)

? ? var max=~~res[1]; ? ?// 最大次數(shù)

? ? var ex=~~res[2]; ? ? // 失效時(shí)間

? ? var dateNow=Date.now(); ? ? // 當(dāng)前時(shí)間

? ? if(n<=0) return done(); ? ? // 調(diào)用頻率過快厨相,直接拒絕(當(dāng)然,還可以有別的不那么簡單粗暴的方法)

? ? function done() {

? ? ? ? fn(null, {

? ? ? ? ? ? total:max,

? ? ? ? ? ? remaining:n<0?0:n,

? ? ? ? ? ? reset:ex

? ? ? ? ?});

? ? }

// 如果還有機(jī)會(huì),則在redis中減去1次蛮穿,順便

? ? db.multi()

? ? .set([count, n-1,'PX', ex*1000-dateNow,'XX'])

? ? .pexpire([limit, ex*1000-dateNow])

? ? .pexpire([reset, ex*1000-dateNow])

? ? .exec(function(err,res) {

? ? ? ? if(err) return fn(err);

? ? ? ? if(isFirstReplyNull(res)) return mget();

? ? ? ? n=n-1;

? ? ? ? done();

? ? });

}

上面有個(gè)pexpire命令庶骄。官方解釋:

這個(gè)命令和 EXPIRE 命令的作用類似,但是它以毫秒為單位設(shè)置 key 的生存時(shí)間践磅,而不像 EXPIRE 命令那樣单刁,以秒為單位

其實(shí)我比較好奇,為什么會(huì)需要改變有效時(shí)間府适。因?yàn)樽畛醯臅r(shí)候已經(jīng)設(shè)置了過期時(shí)間了羔飞。不是很懂。剩下的流程檐春,和之前的一樣褥傍。這里就不必多說了。


三喇聊、總結(jié)

上面說了一大串恍风,總的來說,我算是看得差不多懂了∈睦椋現(xiàn)在來總結(jié)一下這個(gè)流程朋贬,還有看看這個(gè)項(xiàng)目有什么亮點(diǎn)值得學(xué)習(xí)。

流程:

1. mget() ----> create() ---> 返回?cái)?shù)據(jù)

2. mget() ----> decr() -----> 返回?cái)?shù)據(jù)

上面兩個(gè)只是比較粗略的寫法窜骄,實(shí)際上锦募,在這個(gè)項(xiàng)目中,在decr方法里面邻遏,會(huì)考慮到數(shù)據(jù)是否還在糠亩,可能會(huì)再次調(diào)用mget方法。(抱歉准验,我不會(huì)畫圖)

亮點(diǎn):

使用了watch和事務(wù)赎线,代碼雖短,但是也考慮了很多情況糊饱,例如miss內(nèi)存垂寥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市另锋,隨后出現(xiàn)的幾起案子滞项,更是在濱河造成了極大的恐慌,老刑警劉巖夭坪,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件文判,死亡現(xiàn)場離奇詭異,居然都是意外死亡室梅,警方通過查閱死者的電腦和手機(jī)戏仓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門潭流,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柜去,你說我怎么就攤上這事灰嫉。” “怎么了嗓奢?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵讼撒,是天一觀的道長。 經(jīng)常有香客問我股耽,道長根盒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任物蝙,我火速辦了婚禮炎滞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诬乞。我一直安慰自己册赛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布震嫉。 她就那樣靜靜地躺著森瘪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪票堵。 梳的紋絲不亂的頭發(fā)上扼睬,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音悴势,去河邊找鬼窗宇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛特纤,可吹牛的內(nèi)容都是我干的军俊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼叫潦,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蝇完!你這毒婦竟也來了官硝?” 一聲冷哼從身側(cè)響起矗蕊,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氢架,沒想到半個(gè)月后傻咖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岖研,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年卿操,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了警检。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡害淤,死狀恐怖扇雕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窥摄,我是刑警寧澤镶奉,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站崭放,受9級(jí)特大地震影響哨苛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜币砂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一建峭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧决摧,春花似錦亿蒸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拘鞋,卻和暖如春砚蓬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盆色。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工灰蛙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隔躲。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓摩梧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宣旱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仅父,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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