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)存垂寥。