了解redis
redis(remote dictionary server)是一个开源的高性能键值对存储数据库。它支持多种数据结构,包括字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(sorted set)等。redis的特点包括:
- 内存存储:redis将数据存储在内存中,因此读写速度非常快,适用于对性能有较高要求的场景。
- 持久化:redis支持持久化将内存中的数据保存到硬盘上,以便在服务器重启后能够恢复数据。
- 数据结构多样:redis不仅仅支持简单的键值对存储,还支持丰富的数据结构,例如列表、集合、有序集合等,使其具备更多的功能和用途。
- 高并发:redis是单线程模型,通过使用异步i/o和非阻塞i/o来支持高并发。
- 多语言支持:redis支持多种编程语言的客户端,如java、python、c#等,便于开发人员在不同平台上使用。
- 发布/订阅:redis支持发布/订阅模式,允许客户端订阅一个或多个频道并接收对应频道的消息。
- 事务支持:redis支持事务,可以在一个事务中执行多个命令,并保证这些命令的原子性。
由于redis具有高性能、灵活的数据结构和丰富的功能,它被广泛用于缓存、消息队列、计数器、实时排行榜、会话管理等多种应用场景。
需求&为什么需要接口限流
需求:针对相同ip,60s的接口请求次数不能超过10000次
接口限流是为了保护系统和服务,防止因为过多的请求而导致系统过载、性能下降甚至崩溃。以下是进行接口限流的几个主要原因:
- 防止恶意攻击:接口限流可以防止恶意用户或者攻击者通过大量的请求来攻击系统,保护系统的稳定性和安全性。
- 保护系统资源:对于一些计算密集型或者资源消耗较大的接口,限制请求的频率可以避免服务器资源被过度消耗,保障其他正常请求的处理。
- 避免雪崩效应:当某个服务不可用或者响应时间过长时,如果没有限流措施,大量请求可能会涌入后端,导致更多的请求失败,产生雪崩效应。
- 提升系统性能:限流可以控制并发请求数,避免过多的请求导致服务器负载过高,从而提升系统的整体性能和响应速度。
- 提供公平资源分配:通过限流,可以实现对不同用户或者不同服务请求的公平分配,避免某些请求占用过多资源而影响其他请求。
综上所述,进行接口限流是保护系统和提升性能的重要手段,对于高并发的系统尤为重要。通过合理设置限流策略,可以有效地平衡资源利用和系统稳定性,提供更好的用户体验。
实现方案
方案一:固定时间段
思路:
当用户在第一次访问该接口时,向redis中设置一个包含了用户ip和接口方法名的key,value的值初始化为1(表示第一次访问当前接口),同时设置该key的过期时间(60秒),只要此redis的key没有过期,每次访问都将value的值自增1次,用户每次访问接口前,先从redis中拿到当前接口访问次数,如果发现访问次数大于规定的次数(超过10000次),则向用户返回接口访问失败的标识。
实现:
(一)拦截器
1、添加redis依赖:首先在pom.xml文件中添加spring data redis依赖
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>
2、 配置redis连接信息:在application.properties或application.yml中配置redis的连接信息,包括主机、端口、密码等。
3、创建限流拦截器:在项目中创建一个限流拦截器,用于对用户ip进行接口限流。拦截器可以实现handlerinterceptor接口,并重写prehandle方法进行限流逻辑。
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.data.redis.core.redistemplate;
import org.springframework.web.servlet.handlerinterceptor;
import javax.servlet.http.httpservletrequest;
import javax.servlet.http.httpservletresponse;
import java.util.concurrent.timeunit;
public class ratelimitinterceptor implements handlerinterceptor {
@autowired
private redistemplate<string, string> redistemplate;
@override
public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
string ipaddress = getipaddress(request);
string uri = request.getrequesturi().replace("/","_");
string key = "apivisits:" + uri + ":" + ipaddress;
// 判断是否已经达到限流次数
string value = redistemplate.opsforvalue().get(key);
// key 不存在,则是第一次请求设置过期时间
if(stringutils.isblank(value)){
redistemplate.opsforvalue().increment(key, 1);
redistemplate.expire(key, time, timeunit.seconds);
return true;
}
if (value != null && integer.parseint(value) > 10) {
response.setstatus(httpservletresponse.sc_too_many_requests);
return false;
}
// 未达到限流次数,自增
redistemplate.opsforvalue().increment(key, 1);
return true;
}
private string getipaddress(httpservletrequest request) {
// 从请求头或代理头中获取真实ip地址
string ipaddress = request.getheader("x-forwarded-for");
if (ipaddress == null || ipaddress.length() == 0 || "unknown".equalsignorecase(ipaddress)) {
ipaddress = request.getheader("proxy-client-ip");
}
if (ipaddress == null || ipaddress.length() == 0 || "unknown".equalsignorecase(ipaddress)) {
ipaddress = request.getheader("wl-proxy-client-ip");
}
if (ipaddress == null || ipaddress.length() == 0 || "unknown".equalsignorecase(ipaddress)) {
ipaddress = request.getremoteaddr();
}
return ipaddress;
}
}
4、注册拦截器:在配置类中注册自定义的限流拦截器。
import org.springframework.context.annotation.configuration;
import org.springframework.web.servlet.config.annotation.interceptorregistry;
import org.springframework.web.servlet.config.annotation.webmvcconfigurer;
@configuration
public class webmvcconfig implements webmvcconfigurer {
@autowired
private ratelimitinterceptor ratelimitinterceptor;
@override
public void addinterceptors(interceptorregistry registry) {
registry.addinterceptor(ratelimitinterceptor).addpathpatterns("/**");
}
}
(二)aop
以注解+切面的方式实现,将需要进行限流的api加上注解即可
1、创建注解
@target(elementtype.method)
@retention(retentionpolicy.runtime)
public @interface currentlimiting {
/**
* 缓存key
*/
string key() default "apivisits:";
/**
* 限流时间,单位秒
*/
int time() default 5;
/**
* 限流次数
*/
int count() default 10;
}2、创建aop切面
@slf4j
@aspect
@component
@requiredargsconstructor
public class currentlimitingaspect {
private final redistemplate redistemplate;
/**
* 带有注解的方法之前执行
*/
@suppresswarnings("unchecked")
@before("@annotation(currentlimiting)")
public void dobefore(joinpoint point, currentlimiting currentlimiting) throws throwable {
int time = currentlimiting.time();
int count = currentlimiting.count();
// 将接口方法和用户ip构建redis的key
string key = getcurrentlimitingkey(currentlimiting.key(), point);
// 判断是否已经达到限流次数
string value = redistemplate.opsforvalue().get(key);
if (value != null && integer.parseint(value) > count) {
log.error("接口限流,key:{},count:{},currentcount:{}", key, count, value);
throw new runtimeexception("访问过于频繁,请稍后再试!");
}
// 未达到限流次数,自增
redistemplate.opsforvalue().increment(key, 1);
// key 不存在,则是第一次请求设置过期时间
if(stringutils.isblank(value)){
redistemplate.expire(key, time, timeunit.seconds);
}
}
/**
* 组装 redis 的 key
*/
private string getcurrentlimitingkey(string prefixkey,joinpoint point) {
stringbuilder sb = new stringbuilder(prefixkey);
servletrequestattributes attributes = (servletrequestattributes) requestcontextholder.getrequestattributes();
httpservletrequest request = attributes.getrequest();
sb.append( utils.getipaddress(request) );
methodsignature signature = (methodsignature) point.getsignature();
method method = signature.getmethod();
class<?> targetclass = method.getdeclaringclass();
return sb.append("_").append( targetclass.getname() )
.append("_").append(method.getname()).tostring();
}
}缺陷:
当在10:00访问接口,这个时候向reids写入一条数据访问次数为1,在10:59的时候突然访问了9999次,然后redis过期,在11:00访问了9999次,这样出现的问题就是在10:59到11:00之间访问了9999+9999次。故以固定时间段的方式进行限流可能会不起作用,会存在reids过期的临界点内造成大量的用户访问。
方案二:滑动窗口
思路:
由于方案一的时间是固定的,我们可以把固定的时间段改成动态的,也就是在用户每次访问接口时,记录当前用户访问的时间点(时间戳),并计算前一分钟内用户访问该接口的总次数。如果总次数大于限流次数,则不允许用户访问该接口。这样就能保证在任意时刻用户的访问次数不会超过10000次。
实现:
1、创建注解
@target(elementtype.method)
@retention(retentionpolicy.runtime)
public @interface currentlimiting {
/**
* 缓存key
*/
string key() default "apivisits:";
/**
* 限流时间,单位秒
*/
int time() default 5;
/**
* 限流次数
*/
int count() default 10;
}2、创建aop切面
@slf4j
@aspect
@component
@requiredargsconstructor
public class currentlimitingaspect {
private final redistemplate redistemplate;
/**
* 带有注解的方法之前执行
*/
@suppresswarnings("unchecked")
@before("@annotation(currentlimiting)")
public void dobefore(joinpoint point, currentlimiting currentlimiting) throws throwable {
int time = currentlimiting.time();
int count = currentlimiting.count();
// 将接口方法和用户ip构建redis的key
string key = getcurrentlimitingkey(currentlimiting.key(), point);
// 使用zset的 score 设置成用户访问接口的时间戳
zsetoperations zsetoperations = redistemplate.opsforzset();
// 当前时间戳
long currenttime = system.currenttimemillis();
zsetoperations.add(key, currenttime, currenttime);
// 设置过期时间防止key不消失
redistemplate.expire(key, time, timeunit.seconds);
// 移除 time 秒之前的访问记录,动态时间段
zsetoperations.removerangebyscore(key, 0, currenttime - time * 1000);
// 获得当前时间窗口内的访问记录数
long currentcount = zsetoperations.zcard(key);
// 限流判断
if (currentcount > count) {
log.error("接口限流,key:{},count:{},currentcount:{}", key, count, currentcount);
throw new runtimeexception("访问过于频繁,请稍后再试!");
}
}
/**
* 组装 redis 的 key
*/
private string getcurrentlimitingkey(string prefixkey,joinpoint point) {
stringbuilder sb = new stringbuilder(prefixkey);
servletrequestattributes attributes = (servletrequestattributes) requestcontextholder.getrequestattributes();
httpservletrequest request = attributes.getrequest();
sb.append( utils.getipaddress(request) );
methodsignature signature = (methodsignature) point.getsignature();
method method = signature.getmethod();
class<?> targetclass = method.getdeclaringclass();
return sb.append("_").append( targetclass.getname() )
.append("_").append(method.getname()).tostring();
}
}到此这篇关于springboot + redis 实现api接口限流的几种方法的文章就介绍到这了,更多相关springboot redis api接口限流内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论