一、场景举例
我们日常上网冲浪的时候,常常会遇到这样的情景:
- 多次输错登陆密码,再次尝试登录,页面提示需要输入正确的图形验证码
- 某内容平台限定,每个账号单日可发布的文章数量不得超过规定上限
- 采用短信验证码进行交互的场景,M分钟内请求的验证码次数过多,接下来的N分钟里会被拒绝请求,并提示"用户操作频繁,请稍后再试"
当网站的访问量突然很大的时候肯定会对服务器造成影响,甚至无法访问,如果是正常的访问那么很好说明业务量增大可以考虑系统的拓展,但是如果是搜索引擎爬虫频繁访问或是一些恶意访问,那这时候我们就应该限制这些访问的访问次数。redis刚好可以解决这个问题。
二、实现
1.方式一
限制每个用户每分钟最多只能访问100个页面。
实现思路:key使用有“rate.limiting:IP”,value使用数值,用户每次访问将value的值通过INCR命令自增1,如果自增后的值是1同时去设置过期时间为1分钟。这样用户每次访问的时候都会读取该键的值,如果超过100就表明该用户的访问频率超过了限制,需要提示用户稍后操作。且每分钟该键会被自动删除。所以下一分钟又会重新计算,这样就达到了限制访问频率的目的。
伪代码
String key = "rage.limiting:"+ip;
//判断key是否存在
int isExist=exists(key);
//key值存在
if(isExist == 1){
//自增1
int count=incr(key);
if(count > 100){
//超出限制
log.info("访问频率超出限制,请稍后重试");
return;
}
}else{
//key值不存在
multi();//开启事务
incr(key);//key不存在自增1 值为1
expire(key,60);//设置过期时间
exec();//提交事务
}
2.方式二:list
实际上,方式一有个问题:如果一个用户在第一分钟的最后一秒访问了99次,在下一分钟的第一秒访问了100次,那么就相当于在两秒访问了199次,这就与一分钟内最多访问100次相比差距比较大,尽管这种情况比较极端,但是仍然存在。所以如果要实现粒度更小的控制方式,精确到每分钟最多访问100次,那就需要使用到第二种方式。
方式二需要记录用户每次的访问时间,因此对于每个用户,用列表类型的键记录他最近100次访问的时间。如果键中的元素超过100个,就判断时间最早的元素距离现在的时间是否小于1分钟,如果是,则表示用户最近1分钟的访问次数超过100次,如果不是就将当前时间加入列表中,同时把最早的元素删除。
伪代码
String key= "rage.limiting:"+ip;
int listLength=llen(key);
if(listLength<100){
lpush(key,new());
}else{
long time=lindex(key,-1);
if(now()-time < 60){
log.info("超出了访问频率");
}else{
lpush(key,now);
ltrim(key,0.9);
}
}