本文我们演示如何构建产品级的频率限制特性,在Spring Boot应用中使用使用Redis 和 Spring data Redis模块。频率限制通常用于API请求的节流。
频率限制
频率限制就是要限制在给定时间内对特定服务的请求次数。对于产品级的API通常会限制没人每小时的调用次数。下面举例说明:
- 特定手机在一小时内允许 5 个OTP(One-Time Password)
- 网站允许每小时5次忘记密码
- 使用给定API KEY允许20次请求
- 博客站点允许用户(或IP地址)每分钟发布最多1个评论
Redis实现
本文我们构建一个基本频率限制特性,允许每小时每个登录用户请求服务10次。Redis 提供了两个命令 incr
和 expire
,可以很容易实现我们的需求。
我们利用每个用户名每小时创建Redis 建,并确保1小时后自动过期,这样就不会因为过期数据填满我们的数据库。
对于用户名为carvia,下面表格展示Redis键随着时间推移的变化及是否过去情况。
Time | 11:00 | 12:00 | 13:00 | 14:00 |
---|---|---|---|---|
Redis Key (string) | carvia:11 | carvia:12 | carvia:13 | carvia:14 |
Value(值) | 3 | 5 | 10 (max limit) | null |
Expires At(过期时间) | 13:00 (2 hours later) | 14:00 | 15:00 | 16:00 |
Redis键是由用户名和时间数字通过冒号组合而成。并设置2个小时后过期,所以不用担心Redis存储空间。
伪代码实现:
- GET [username]:[当前小时]
- 如果结果存在且小于10,调转到步骤4,否则进入步骤4
- 显示达到最大限制错误信息并结束
- Redis开始事务,执行下面步骤
- 使用
incr
增加[username]:[当前小时]键的计数器 - 对于键设置过期时间为2小时从现在,使用
expire[username]:[当前小时]3600
- 使用
- 允许请求继续服务
Spring Boot 应用实现
Spring Data Redis 提供简单的配置及访问方式,同时包括对Redis存储交互的低级和高级封装抽象。下面我们创建Spring Boot 用于实现频率限制特性。
在docker中启动Redis
docker run -itd --name redis -p 6379:6379 --rm redis
# 用客户端端来连接redis
redis-cli
可以在idea中利用docker插件访问redis,连接客户端进行测试。
引用依赖
Spring Boot 版本及主要依赖包。Java Redis 客户端默认使用 Lettuce,当然你也可以使用Jedis。
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
配置Redis连接
在 application.properties
增加相应配置:
# redis 服务器地址(安装在虚拟机中的docker)
spring.redis.host=192.168.31.93
spring.redis.database=0
spring.redis.password=
这样我们启动了对Redis的自动配置。Spring Boot自动会注入 StringRedisTemplate
bean ,利用它和Redis进行交互。
实现代码
为了简化,我们直接写一个类进行测试:
package com.dataz.ratelimit.service;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @Author Tommy
* @create 2021/6/5 16:18
*/
@RestController
public class RateLimit {
private static final Logger logger = LoggerFactory.getLogger(RateLimit.class);
private static final int REQUESTS_PER_HOUR = 10;
private static final int TEST_PER_HOUR = 20;
private static final String USER_NAME = "carvia";
private final StringRedisTemplate stringTemplate;
public RateLimit(StringRedisTemplate stringTemplate) {
this.stringTemplate = stringTemplate;
}
private boolean isAllowed(String username) {
final int hour = LocalDateTime.now().getHour();
String key = username + ":" + hour;
ValueOperations<String, String> operations = stringTemplate.opsForValue();
String requests = operations.get(key);
if (StringUtils.isNotBlank(requests) && Integer.parseInt(requests) >= REQUESTS_PER_HOUR) {
return false;
}
List<Object> txResults = stringTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {
final StringRedisTemplate redisTemplate = (StringRedisTemplate) operations;
final ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
operations.multi();
valueOperations.increment(key);
redisTemplate.expire(key, 2, TimeUnit.HOURS);
// This will contain the results of all operations in the transaction
return operations.exec();
}
});
logger.info("Current request count:{} ", Objects.requireNonNull(txResults.get(0),"null"));
return true;
}
@GetMapping("/api/service01")
public ResponseEntity<String> service() {
for (int i=0; i< TEST_PER_HOUR; i++) {
boolean allowed = isAllowed(USER_NAME);
if(!allowed) {
return ResponseEntity.ok("超过限制");
}
}
return ResponseEntity.ok("正常访问");
}
}
启动应用,发送请求进行测试:
GET http://localhost:8080/api/service01?userName=jack
执行结果返回 超过限制
。
查看Redis中是否由对应键,且值达到最大值。
127.0.0.1:6379> keys ja*
1) "jack:17"
127.0.0.1:6379> get jack:17
"10"
总结
本文利用Redis在Spring Boot应用实现频率限制。对于不复杂的频率限制通过本文实现比较容易,复杂场景需要更专业工具实现,如: Bucket4j
。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/117601450