redis实现访问频次限制的几种方式

转自:http://blog.csdn.net/yanghua_kobe/article/details/47904663

结合上一篇文章《redis在学生抢房应用中的实践小结》中提及的用redis实现DDOS设计时遇到的expire的坑。其实,redis官网中对incr命令的介绍中已经有关于如何用redis来做rate limit的探讨。这里将实现的两种模式翻译一下,并适当加了一些批注说明,原文可见官网

模式:Rate limiter

频次限制器模式是一种特殊的计数器,它常被用来限制某个操作可以被执行的频次。这个模式的实质其实是限制对一个公共API执行访问请求的次数限制。我们使用incr命令提供该模式的两种实现。这里我们假设需要解决的问题是:对每个IP,限制对某API的调用次数最高位10次每秒。

模式:Rate limiter 1

对该模式一个相对简单和直接的实现,请见如下代码:

<code class="hljs vbscript has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">FUNCTION</span> LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">":"</span>+ts
current = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">GET</span>(keyname)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">IF</span> current != <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">NULL</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">AND</span> current > <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">THEN</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ERROR</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"too many requests per second"</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ELSE</span>
    MULTI
        INCR(keyname,<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>)
        EXPIRE(keyname,<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>)
    EXEC
    PERFORM_API_CALL()
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">END</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li></ul>

简单来说,我们对每个IP的每一秒都有一个计数器,但每个计数器都有一个额外的设置:它们都将被设置一个10秒的过期时间。这可以使得当时间已经不是当前秒时(此时该计数器也无效了),能够让redis自动移除它。

需要注意的是,这里我们使用multiexec命令来确保对每个API调用既执行了incr也同时能够执行expire命令。

multi命令用于标识一个命令集被包含在一个事务块中,exec保证该事务块命令集执行的原子性。

模式:Rate limiter 2

另外的一种实现是采用单一的计数器,但是为了避免race condition(竞态条件),它也更复杂。我们来看几种不同的变体:

<code class="hljs vbscript has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">FUNCTION</span> LIMIT_API_CALL(ip):
current = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">GET</span>(ip)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">IF</span> current != <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">NULL</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">AND</span> current > <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">THEN</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ERROR</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"too many requests per second"</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ELSE</span>
    value = INCR(ip)
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">IF</span> value == <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">THEN</span>
        EXPIRE(value,<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>)
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">END</span>
    PERFORM_API_CALL()
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">END</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li></ul>

该计数器在当前秒内第一次请求被执行时创建,但它只能存活一秒。如果在当前秒内,发送超过10次请求,那么该计数器将超过10。否则它将失效并从0开始重新计数。

在上面的代码中,存在一个race condition。如果因为某个原因,上面的代码只执行了incr命令,却没有执行expire命令,那么这个key将会被泄漏,直到我们再次遇到相同的ip(备注,如果这里没有辅助的删除该key的措施,那么该key将永不过期,也将每次都发生错误,详情可见本人之前一篇文章)。

这种问题也不难处理,可以将incr命令以及另外的expire命令打包到一个lua脚本里,该脚本可以用eval命令提交给redis执行(该方式只在redis版本大于等于2.6之后才能支持)。

<code class="hljs sql has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">local current
current = redis.<span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">call</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"incr"</span>,KEYS[<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>])
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> tonumber(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">current</span>) == <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">then</span>
    redis.<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">call</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"expire"</span>,KEYS[<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>],<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">end</span></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li></ul>

当然,也有另一种方式来解决这个问题而不需要动用lua脚本,但需要用redis的list数据结构来替代计数器。这种实现方式将会更复杂,并使用更高级的特性。但它有一个好处是记住调用当前API的每个客户端的IP。这种方式可能很有用也可能没用,这取决于应用需求。

<code class="hljs vbnet has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">FUNCTION</span> LIMIT_API_CALL(ip)
current = LLEN(ip)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">IF</span> current > <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">THEN</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ERROR</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"too many requests per second"</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ELSE</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">IF</span> EXISTS(ip) == <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">FALSE</span>
        MULTI
            RPUSH(ip,ip)
            EXPIRE(ip,<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>)
        EXEC
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">ELSE</span>
        RPUSHX(ip,ip)
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">END</span>
    PERFORM_API_CALL()
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">END</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li></ul>

rpushx命令只在key存在时才会将值加入list

仍然需要注意的是,这里也存在一个race condition(但这却不会产生太大的影响)。问题是:exists可能返回false,但在我们执行multi/exec块内的创建list的代码之前,该list可能已被其他客户端创建。然而,在这个race condition发生时,将仅仅只是丢失一个API调用,所以rate limiting仍然工作得很好。

这里产生race condition不会有大问题的原因在于,else分支使用的rpushx,它不会导致if not than init的问题,并且expire命令将在创建list的时候以原子的形式捆绑执行。不会产生key泄漏,导致永不失效的情况产生。

更多内容请访问:http://vinoyang.com


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值