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

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

模式:Rate limiter

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

模式:Rate limiter 1

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

FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts
current = GET(keyname)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    MULTI
        INCR(keyname,1)
        EXPIRE(keyname,10)
    EXEC
    PERFORM_API_CALL()
END

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

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

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

模式:Rate limiter 2

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

FUNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    value = INCR(ip)
    IF value == 1 THEN
        EXPIRE(value,1)
    END
    PERFORM_API_CALL()
END

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

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

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

local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],1)
end

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

FUNCTION LIMIT_API_CALL(ip)
current = LLEN(ip)
IF current > 10 THEN
    ERROR "too many requests per second"
ELSE
    IF EXISTS(ip) == FALSE
        MULTI
            RPUSH(ip,ip)
            EXPIRE(ip,1)
        EXEC
    ELSE
        RPUSHX(ip,ip)
    END
    PERFORM_API_CALL()
END

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泄漏,导致永不失效的情况产生。

原文发布时间为:2015-08-23

本文来自合作伙伴CSDN博客,了解相关信息可以关注CSDN博客。

时间: 2024-10-06 07:11:37

redis实现访问频次限制的几种方式的相关文章

C基础 redis缓存访问详解_C 语言

引言 先说redis安装, 这里采用的环境是. Linux version 4.4.0-22-generic (buildd@lgw01-41) (gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2) ) #40-Ubuntu SMP Thu May 12 22:03:46 UTC 2016 对于 ubuntu 安装 redis是非常简单的. 这里采用源码安装. 安装代码如下 wget http://download.redis.io/relea

优云运维:漫谈redis在运维数据分析中的去重统计方式

今天,我和大家分享下redis在运维数据分析中的去重统计方式.为了避免混淆,本文中对于redis的数据结构做如下约定: SET:saddkey member ZSET:zaddkeyscoremember HYPERLOGLOG:pfaddkeyelement STRING:setbitkeyoffset value 名词约定: 维度:比如版本.操作系统类型.操作系统版本.运营商.设备型号.网络类型等 复合维度:由两个或多个维度交错产生的维度,比如某个版本下的某个设备型号. 去重统计在数据化运维

javascript面向对象之访问对象属性的两种方式分析_javascript技巧

本文实例分析了javascript面向对象之访问对象属性的两种方式.分享给大家供大家参考.具体如下: javascript面向对象的访问对象属性的两种方式.如下代码所示: 复制代码 代码如下: <script language="javascript" type="text/javascript"> function Person(){}; var p1 = new Person(); p1.name="王美人"; document.

Hadoop文件系统访问的两种方式

在这里记录下学习hadoop 的过程,并对重要内容记录下来,以备以后查漏补缺. 要从Hadoop文件系统中读取文件,一般有两种方式: 1.使用java.net.URL对象 package com.ytu.chapter3; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import org.apache.hado

Android中访问sdcard路径的几种方式

以前的Android(4.1之前的版本)中,SDcard路径通过"/sdcard"或者"/mnt/sdcard"来表示,而在JellyBean(安卓4.1)系统中修改为了" /storage/sdcard0",还会有多个SDcard的情况.目前为了保持和之前代码的兼容,SDcard路径做了Link映射.为了使代码更加健壮并能兼容以后的Android版本和新设备,安卓4,1后sdcard中会有系统自动生成的保存特定内容的的文件目录,从而可以使用一些

java实现redis数据库访问

分析亚马逊AWS数据存储-----http://edu.csdn.net/course/detail/873 一.server端安装 1.下载 https://github.com/MSOpenTech/redis 可看到当前可下载版本:redis2.6 下载windows平台文件: 解压后,选择当前64位win7系统对应的版本: 2.安装 1)解压后将里面所有文件拷贝至redis安装目录: 几个exe程序的功能:   redis-benchmark.exe:性能测试,用以模拟同时由N个客户端发

利用redis缓存热门数据,分页的一种思路

普通分页 一般分页做缓存都是直接查找出来,按页放到缓存里,但是这种缓存方式有很多缺点. 如缓存不能及时更新,一旦数据有变化,所有的之前的分页缓存都失效了. 比如像微博这样的场景,微博下面现在有一个顶次数的排序.这个用传统的分页方式很难应对. 一种思路 最近想到了另一种思路. 数据以ID为key缓存到Redis里: 把数据ID和排序打分存到Redis的skip list,即zset里: 当查找数据时,先从Redis里的skip list取出对应的分页数据,得到ID列表. 用multi get从re

SpringMVC访问静态资源的三种方式

 如果你的DispatcherServlet拦截 *.do这样的URL,就不存在访问不到静态资源的问题.如果你的DispatcherServlet拦截"/",拦截了所有的请求,同时对*.js,*.jpg的访问也就被拦截了. 问题原因:罪魁祸首是web.xml下对spring的DispatcherServlet请求url映射的配置,原配置如下: [html] view plain copy   <servlet>       <servlet-name>sprin

php使用Smarty的相关注意事项及访问变量的几种方式_php模板

$tpl=new Smarty();//新建一个smarty对象,我使用的是Smarty-3.1.6版本1.设置smarty模板路径$tpl->setTemplateDir():默认情况下是templates2.设置smarty模板编译路径$tpl->setCompileDir();默认情况下是templates_c3.设置smarty模板引擎的左右 分隔符,        $tpl->left_delimiter="<{";        $tpl->r