github地址:传送门
一、简介
这是一个nodejs版本的接口频率算法----令牌桶算法。在P时间段里,只能被调用N次。这段时间过后,又重新有了N次机会。(这个算法有点不是很完美,因为能在极短的时间内,发起2N次请求,可能会给服务器带来一定的压力)
二、源码分析
这里的分析,以函数为一个小的基本单元来进行。
var assert=require('assert');
这里引入的是nodejs的断言模块,当不符合预期的时候,会抛出异常。
function Limiter(opts) {
this.id=opts.id; // 唯一标识,如用户id
this.db=opts.db; // redis数据库实例
assert(this.id,'.id required');
assert(this.db,'.db required');
this.max=opts.max||2500; // 默认可调用次数(N)
this.duration=opts.duration||3600000; // 默认间隔时间(P,一小时)
this.prefix='limit:'+this.id+':'; // redis的key
}
上面是一个Limiter类,在初始化的时候传入一系列的配置。
Limiter.prototype.inspect=function() {
return'
+this.id+', duration='
+this.duration+', max='
+this.max+'>';
};
这个方法,方便效果的展示
// 判断第一个值是不是为空(这里指的是key: "limit:<id>:count"对应的值),如果不存在的话,表示redis没有这个记录,需要重新分配次数和时间给当前用户
function isFirstReplyNull(replies) {
if (!replies) {
return true;
}
return Array.isArray(replies[0]) ?
// ioredis
!replies[0][1] :
// node_redis
!replies[0];
}
// 这个是核心方法
Limiter.prototype.get = function (fn) {
var count = this.prefix + 'count'; // 剩余次数
var limit = this.prefix + 'limit'; // 最多次数
var reset = this.prefix + 'reset'; // 失效时间
var duration = this.duration; // 间隔时间
var max = this.max;
var db = this.db;
function create() {
// 为当前用户开辟一块新的内存,保存调用情况。总共有三个key值,分别为上面的count、limit、reset
}
function decr(res) {
// 收到用户的请求,进行计算,如果允许访问,则减少一次机会,否则直接返回
}
function mget() {
// 调用这个方法直接,redis中肯定会存有该用户相关情况,如果不存在的话,就调用create方法;存在的话,调用decr方法,在库存中减去一次。
}
mget();
};
下面分开来讲解上面提到的三个方法。
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);
});
});
}
上面用到一个 watch 命令。
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)
在create方法和decr方法里面,都会使用multi和exec命令。我们要确保两个方法不能同时修改count值,所以,我们需要加上这个指令。
如果没有分配内存,就调用create方法进行分配,否则就直接调用方法decr去库存。
create()
function create() {
var ex = (Date.now() + duration) / 1000 | 0; // 失效时间
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
});
});
}
上面这个方法也很好理解。首先计算失效时间ex,然后依次往这三个key赋值。如果恰好碰到内存不见了(这三个key没有了,至于为什么会没有,也许是redis不小心被清空了,反正就是突然没了),就调用mget方法(等于是重新跑一次这个流程)。否则,就返回分配好的内存,告诉调用者最大次数total,剩余次数remaining, 失效时间reset。
decr()
function decr(res) {
var n=~~res[0]; // 剩余次数
var max=~~res[1]; // 最大次数
var ex=~~res[2]; // 失效时间
var dateNow=Date.now(); // 当前时间
if(n<=0) return done(); // 调用频率过快,直接拒绝(当然,还可以有别的不那么简单粗暴的方法)
function done() {
fn(null, {
total:max,
remaining:n<0?0:n,
reset:ex
});
}
// 如果还有机会,则在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();
});
}
上面有个pexpire命令。官方解释:
这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位
其实我比较好奇,为什么会需要改变有效时间。因为最初的时候已经设置了过期时间了。不是很懂。剩下的流程,和之前的一样。这里就不必多说了。
三、总结
上面说了一大串,总的来说,我算是看得差不多懂了。现在来总结一下这个流程,还有看看这个项目有什么亮点值得学习。
流程:
1. mget() ----> create() ---> 返回数据
2. mget() ----> decr() -----> 返回数据
上面两个只是比较粗略的写法,实际上,在这个项目中,在decr方法里面,会考虑到数据是否还在,可能会再次调用mget方法。(抱歉,我不会画图)
亮点:
使用了watch和事务,代码虽短,但是也考虑了很多情况,例如miss内存。