Spring Boot+Redis 实现一个简单的限流器
文章目录
- Spring Boot+Redis 实现一个简单的限流器
- 0.前言
- 1.基础介绍
- 2.步骤
- 2.1. 引入依赖
- 2.2. 配置文件
- 2.3. 核心源码
- 优化后
- 再优化一下加入布隆过滤器
- 4.总结
- 5.参考文档
0.前言
在Spring Boot中使用Redis和过滤器实现请求限流。过滤器将在每个请求到达时检查请求频率,并根据设定的阈值进行限制。这样可以保护您的应用程序免受恶意请求或高并发请求的影响。请根据您的具体需求和业务场景进行适当的修改和扩展。
1.基础介绍
1. 限流场景
假设我们有一个API接口,需要限制每个用户在一段时间内的请求频率。比如每秒只允许请求100次等等的业务需求。
2. 实现限流逻辑:
使用Redis的计数器功能可以实现基于时间窗口的限流算法。通过在Redis中存储请求计数器和过期时间,可以控制单位时间内的请求频率。在需要进行限流的接口或方法中,使用Redis的原子操作(如INCR和EXPIRE)来增加计数器并设置过期时间。
在每个请求到达时,检查计数器的值是否超过设定的阈值,如果超过则拒绝请求,否则允许请求继续执行。
本文我们通过Spring Boot +Redis 实现一个轻量级的消息队列。
2.步骤
2.1. 引入依赖
<dependencies>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2.2. 配置文件
# Redis连接配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=your_password
spring.redis.database=0
# Redis连接池配置
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-wait=-1
在上面的配置中,您可以根据实际情况修改以下属性:
-
spring.redis.host
:Redis服务器的主机名或IP地址。 -
spring.redis.port
:Redis服务器的端口号。 -
spring.redis.password
:Redis服务器的密码(如果有的话)。 -
spring.redis.database
:Redis数据库的索引,默认为0。
另外,您还可以配置Redis连接池的属性,以控制连接池的行为。在示例配置中,设置了以下连接池属性:
-
spring.redis.jedis.pool.max-active
:连接池中的最大活动连接数。 -
spring.redis.jedis.pool.max-idle
:连接池中的最大空闲连接数。 -
spring.redis.jedis.pool.min-idle
:连接池中的最小空闲连接数。 -
spring.redis.jedis.pool.max-wait
:从连接池获取连接的最大等待时间(毫秒),-1表示无限等待。
如果 使用的是YAML格式的配置文件(application.yml
),可以将上述配置转换为相应的格式:
spring:
redis:
host: 127.0.0.1
port: 6379
password: your_password
database: 0
redis.jedis.pool:
max-active: 50
max-idle: 10
min-idle: 5
max-wait: -1
请根据您的实际Redis服务器配置进行调整,并根据需要添加其他相关配置,如超时设置、SSL配置等。
2.3. 核心源码
- 实现请求限流过滤器:
创建一个实现javax.servlet.Filter
接口的请求限流过滤器。在过滤器中,使用Redis的计数器功能来实现请求限流逻辑。
示例中,RequestLimitFilter
是一个实现了javax.servlet.Filter
接口的请求限流过滤器。它使用Redis的计数器功能来实现请求限流逻辑。每个请求到达时,根据客户端的IP地址作为Redis的键,增加计数器的值并设置过期时间为指定的时间窗口。如果计数器超过了设定的阈值(这里是100),则返回HTTP 429 Too Many Requests
响应。
示例中使用的是RedisTemplate<String, String>
来操作Redis, 可以根据需要调整为适合您的数据类型和操作方式的RedisTemplate。
@Component
public class RequestLimitFilter implements Filter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String REQUEST_LIMIT_PREFIX = "requestLimit:";
private static final long REQUEST_LIMIT = 100; // 请求限制数量
private static final long TIME_WINDOW = 60; // 时间窗口(单位:秒)
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ipAddress = getClientIpAddress(httpRequest);
String key = REQUEST_LIMIT_PREFIX + ipAddress;
Long counter = redisTemplate.opsForValue().increment(key, 1);
if (counter == 1) {
redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
}
if (counter > REQUEST_LIMIT) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.getWriter().write("请求频率超过限制,请稍后再试!");
return;
}
chain.doFilter(request, response);
}
private String getClientIpAddress(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
}
优化后
public class RequestLimitFilter implements Filter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String REQUEST_LIMIT_PREFIX = "requestLimit:";
private static final long REQUEST_LIMIT = 100; // 请求限制数量
private static final long TIME_WINDOW = 60; // 时间窗口(单位:秒)
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ipAddress = getClientIpAddress(httpRequest);
String key = REQUEST_LIMIT_PREFIX + ipAddress;
Long counter = redisTemplate.opsForValue().increment(key, 1);
if (counter == 1) {
redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
}
if (counter > REQUEST_LIMIT) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
try (PrintWriter writer = httpResponse.getWriter()) {
writer.write("请求频率超过限制,请稍后再试!");
}
return;
}
chain.doFilter(request, response);
}
private String getClientIpAddress(HttpServletRequest request) {
...
return ipAddress;
}
}
再优化一下加入布隆过滤器
使用布隆过滤器减少对Redis的访问:布隆过滤器是一种高效的概率数据结构,可以用于快速判断元素是否存在于集合中。在限制请求频率时,可以使用布隆过滤器来减少对Redis的访问。只有在布隆过滤器判断请求不是重复请求时,才进行Redis操作。
public class RequestLimitFilter implements Filter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String REQUEST_LIMIT_PREFIX = "requestLimit:";
private static final long REQUEST_LIMIT = 100; // 请求限制数量
private static final long TIME_WINDOW = 60; // 时间窗口(单位:秒)
private BloomFilter<String> bloomFilter;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化布隆过滤器
int expectedInsertions = 1000; // 预期插入数量
double falsePositiveProbability = 0.01; // 误判率
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, falsePositiveProbability);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ipAddress = getClientIpAddress(httpRequest);
if (bloomFilter.mightContain(ipAddress)) {
// 布隆过滤器判断可能是重复请求,直接放行
chain.doFilter(request, response);
return;
}
String key = REQUEST_LIMIT_PREFIX + ipAddress;
Long counter;
boolean isNewKey = false;
try {
counter = redisTemplate.opsForValue().increment(key, 1);
if (counter == 1) {
redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
isNewKey = true;
}
} catch (Exception e) {
// 处理Redis操作异常
// 可以选择记录日志或采取适当的处理措施
e.printStackTrace();
chain.doFilter(request, response);
return;
}
if (counter > REQUEST_LIMIT) {
if (isNewKey) {
// 删除新创建的键,避免无限增长
redisTemplate.delete(key);
}
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
try (PrintWriter writer = httpResponse.getWriter()) {
writer.write("请求频率超过限制,请稍后再试!");
}
return;
}
bloomFilter.put(ipAddress); // 将IP地址添加到布隆过滤器
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 清理资源,如关闭Redis连接等
}
private String getClientIpAddress(HttpServletRequest request) {
// 获取客户端IP地址的逻辑
// ...
}
}
在上述代码中,我们引入了布隆过滤器来减少对Redis的访问。如果布隆过滤器判断请求可能是重复请求,则直接放行,无需进行Redis操作。同时,我们还添加了对Redis操作异常的处理,并在限流超过阈值时删除新创建的键,以避免无限增长。请根据实际情况进行适当调整和完善。
- 注册过滤器:
在Spring Boot应用程序的配置类中注册过滤器,以便它能够在请求处理过程中生效。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RequestLimitFilter requestLimitFilter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLimitFilter);
}
}
通过将过滤器添加到addInterceptors
方法中,它将被注册为Spring Boot应用程序的全局过滤器,并在请求到达时执行限流逻辑。
4.总结
其实上面我们写完的还是有问题的
- 如果系统部署在多个节点上,可以考虑使用分布式限流算法,如令牌桶算法或漏桶算法。这些算法可以在分布式环境中平衡请求的处理,并保证全局的请求限制。
- 将请求限流的参数,如请求限制数量和时间窗口,配置为可动态调整的参数。可以使用注解或配置文件来管理这些参数,以便在运行时进行调整,而无需重新编译代码。
5.参考文档
- Spring Data Redis官方文档:https://docs.spring.io/spring-data/redis/docs/current/reference/html/ ↗ 这个文档提供了关于如何在Spring Boot中使用Spring Data Redis进行Redis操作的详细指南。 可以了解如何配置Redis连接、使用RedisTemplate进行操作以及其他高级功能。