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

2022-05-24 10:18:02

本文我们演示如何构建产品级的频率限制特性,在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键随着时间推移的变化及是否过去情况。

Time11:0012:0013:0014:00
Redis Key (string)carvia:11carvia:12carvia:13carvia:14
Value(值)3510 (max limit)null
Expires At(过期时间)13:00 (2 hours later)14:0015:0016: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 -p6379: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进行交互。

实现代码

为了简化,我们直接写一个类进行测试:

packagecom.dataz.ratelimit.service;importorg.apache.commons.lang3.StringUtils;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.dao.DataAccessException;importorg.springframework.data.redis.core.RedisOperations;importorg.springframework.data.redis.core.SessionCallback;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.time.LocalDateTime;importjava.util.List;importjava.util.Objects;importjava.util.concurrent.TimeUnit;/**
 * @Author Tommy
 * @create 2021/6/5 16:18
 */@RestControllerpublicclassRateLimit{privatestaticfinalLogger logger=LoggerFactory.getLogger(RateLimit.class);privatestaticfinalint REQUESTS_PER_HOUR=10;privatestaticfinalint TEST_PER_HOUR=20;privatestaticfinalString USER_NAME="carvia";privatefinalStringRedisTemplate stringTemplate;publicRateLimit(StringRedisTemplate stringTemplate){this.stringTemplate= stringTemplate;}privatebooleanisAllowed(String username){finalint 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){returnfalse;}List<Object> txResults= stringTemplate.execute(newSessionCallback<List<Object>>(){@Overridepublic<K,V>List<Object>execute(RedisOperations<K,V> operations)throwsDataAccessException{finalStringRedisTemplate redisTemplate=(StringRedisTemplate) operations;finalValueOperations<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 transactionreturn operations.exec();}});
        logger.info("Current request count:{} ",Objects.requireNonNull(txResults.get(0),"null"));returntrue;}@GetMapping("/api/service01")publicResponseEntity<String>service(){for(int i=0; i< TEST_PER_HOUR; i++){boolean allowed=isAllowed(USER_NAME);if(!allowed){returnResponseEntity.ok("超过限制");}}returnResponseEntity.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
    更新时间:2022-05-24 10:18:02