美文网首页
node-ratelimiter

node-ratelimiter

作者: 一溪酒 | 来源:发表于2016-09-20 00:12 被阅读362次

    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内存。

    相关文章

      网友评论

          本文标题:node-ratelimiter

          本文链接:https://www.haomeiwen.com/subject/zegnettx.html