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

本文我们演示如何构建产品级的频率限制特性,在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.8java.version>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASEspring-boot.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.1version>
        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

你可能感兴趣的:(spring,boot,redis)