Skip to main content
 首页 » 编程设计

Spring Boot应用利用Redis实现频率限制

2022年07月19日166lori

本文我们演示如何构建产品级的频率限制特性,在Spring Boot应用中使用使用Redis 和 Spring data Redis模块。频率限制通常用于API请求的节流。

频率限制

频率限制就是要限制在给定时间内对特定服务的请求次数。对于产品级的API通常会限制没人每小时的调用次数。下面举例说明:

  • 特定手机在一小时内允许 5 个OTP(One-Time Password)
  • 网站允许每小时5次忘记密码
  • 使用给定API KEY允许20次请求
  • 博客站点允许用户(或IP地址)每分钟发布最多1个评论

Redis实现

本文我们构建一个基本频率限制特性,允许每小时每个登录用户请求服务10次。Redis 提供了两个命令 increxpire ,可以很容易实现我们的需求。

我们利用每个用户名每小时创建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存储空间。

伪代码实现:

  1. GET [username]:[当前小时]
  2. 如果结果存在且小于10,调转到步骤4,否则进入步骤4
  3. 显示达到最大限制错误信息并结束
  4. Redis开始事务,执行下面步骤
    • 使用 incr 增加[username]:[当前小时]键的计数器
    • 对于键设置过期时间为2小时从现在,使用 expire[username]:[当前小时]3600
  5. 允许请求继续服务

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