SpringBoot中使用Redis对接口进行限流的实现

 更新时间:2021年12月29日 09:46:14   作者:The_SHY  
本文将结合实例代码,介绍SpringBoot中使用Redis对接口进行限流的实现,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

一个基于Redis实现的接口限流方案,先说要实现的功能

  • 可以限制指定的接口,在一定时间内,只能被请求N次,超过次数就返回异常信息
  • 可以通过配置文件,或者管理后台,动态的修改限流配置

实现的思路

使用 Hash 存储接口的限流配置

request_limit_config    "/api2" : {"limit": 10, "time": 1, "timeUnit": "SECONDS"}

hash中的key就是请求的uri路径,value是一个对象。通过3个属性,描述限制策略

  • limit 最多请求次数
  • time 时间
  • timeUnit 时间单位

使用普通kv,存储api的请求次数

request_limit:/api  1

处理请求的时候,通过increment对该key进行 +1 操作,如果返回1,则表示是第一次请求,此时设置它的过期时间。为限制策略中定义时间限制信息。再通过命名的返回值,判断是否超出了限制。

increment 指令是线程安全的,不用担心并发的问题。

使用SpringBoot实现

创建SpringBoot工程,添加

spring-boot-starter-data-redis依赖,并且给出正确的配置。

这里不做工程的创建,配置,以及其他额外代码的演示,仅仅给出关键的代码。

RedisKeys

定义两个Key,限流用到的2个Key

public interface RedisKeys {
    /**
     * api的限制配置,hash key
     */
    String REQUEST_LIMIT_CONFIG = "request_limit_config";

    /**
     * api的请求的次数
     */
    String REQUEST_LIMIT = "request_limit";
}

ObjectRedisTemplate

为了提高hash value的序列化效率,自定义一个RedisTemplate的实现。使用jdk的序列化,而不是json。

import org.springframework.data.redis.core.RedisTemplate;

public class ObjectRedisTemplate extends RedisTemplate<String, Object> {

}

RedisConfigration

把自定义的ObjectRedisTemplate配置到IOC

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;

import io.springboot.jwt.redis.ObjectRedisTemplate;

@Configuration
public class RedisConfiguration {
    @Bean
    public ObjectRedisTemplate objectRedisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory) {

        ObjectRedisTemplate objectRedisTemplate = new ObjectRedisTemplate();
        objectRedisTemplate.setConnectionFactory(redisConnectionFactory);

        objectRedisTemplate.setKeySerializer(RedisSerializer.string());
        objectRedisTemplate.setValueSerializer(RedisSerializer.java());

        // hash的key使用String序列化
        objectRedisTemplate.setHashKeySerializer(RedisSerializer.string());
        // hash的value使用jdk的序列化
        objectRedisTemplate.setHashValueSerializer(RedisSerializer.java());
        return objectRedisTemplate;
    }
}

RequestLimitConfig

用于描述限制策略的对象。

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

public class RequestLimitConfig implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1101875328323558092L;

    // 最大请求次数
    private long limit;
    // 时间
    private long time;
    // 时间单位
    private TimeUnit timeUnit;
    public RequestLimitConfig() {
        super();
    }
    public RequestLimitConfig(long limit, long time, TimeUnit timeUnit) {
        super();
        this.limit = limit;
        this.time = time;
        this.timeUnit = timeUnit;
    }
    public long getLimit() {
        return limit;
    }
    public void setLimit(long limit) {
        this.limit = limit;
    }
    public long getTime() {
        return time;
    }
    public void setTime(long time) {
        this.time = time;
    }
    public TimeUnit getTimeUnit() {
        return timeUnit;
    }
    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }
    @Override
    public String toString() {
        return "RequestLimitConfig [limit=" + limit + ", time=" + time + ", timeUnit=" + timeUnit + "]";
    }
}

RequestLimitInterceptor

通过拦截器,来完成限流的实现。

import java.nio.charset.StandardCharsets;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import io.springboot.jwt.redis.ObjectRedisTemplate;
import io.springboot.jwt.redis.RedisKeys;
import io.springboot.jwt.web.RequestLimitConfig;

public class RequestLimitInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(RequestLimitInterceptor.class);

    @Autowired
    private ObjectRedisTemplate objectRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        /**
         * 获取到请求的URI
         */
        String contentPath = request.getContextPath();
        String uri = request.getRequestURI().toString();
        if (!StringUtils.isEmpty(contentPath) && !contentPath.equals("/")) {
            uri =  uri.substring(uri.indexOf(contentPath) + contentPath.length());
        }
        LOGGER.info("uri={}",  uri);

        /**
         * 尝试从hash中读取得到当前接口的限流配置
         */
        RequestLimitConfig requestLimitConfig = (RequestLimitConfig) this.objectRedisTemplate.opsForHash().get(RedisKeys.REQUEST_LIMIT_CONFIG, uri);
        if (requestLimitConfig == null) {
            LOGGER.info("该uri={}没有限流配置", uri);
            return true;
        }

        String limitKey = RedisKeys.REQUEST_LIMIT + ":" + uri;

        /**
         * 当前接口的访问次数 +1
         */
        long count = this.objectRedisTemplate.opsForValue().increment(limitKey);
        if (count == 1) {
            /**
             * 第一次请求,设置key的过期时间
             */
            this.objectRedisTemplate.expire(limitKey, requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit());
            LOGGER.info("设置过期时间:time={}, timeUnit={}", requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit());
        }

        LOGGER.info("请求限制。limit={}, count={}", requestLimitConfig.getLimit(), count);

        if (count > requestLimitConfig.getLimit()) {
            /**
             * 限定时间内,请求超出限制,响应客户端错误信息。
             */
            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.getWriter().write("服务器繁忙,稍后再试");
            return false;
        }
        return true;
    }
}

Controller

一个用于测试的接口类

import java.util.Collections;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping
    public Object test () {
        return Collections.singletonMap("success", true);
    }
}

WebMvcConfigration

拦截器的配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import io.springboot.jwt.web.interceptor.RequestLimitInterceptor;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.requestLimitInterceptor())
            .addPathPatterns("/test");
    }

    @Bean
    public RequestLimitInterceptor requestLimitInterceptor() {
        return new RequestLimitInterceptor();
    }
}

通过@Test测试,初始化一个限流配置

@Autowired
private ObjectRedisTemplate objectRedisTemplate;

@Test
public void test () {
    // 3秒内,只能请求2次
    RequestLimitConfig requestLimitConfig = new RequestLimitConfig(2, 3, TimeUnit.SECONDS);
    // 限制的uri是 /test
    this.objectRedisTemplate.opsForHash().put(RedisKeys.REQUEST_LIMIT_CONFIG, "/test", requestLimitConfig);
}

使用浏览器演示

最后一些问题

怎么灵活的配置

都写到这个份儿上了,如果熟悉Redis以及客户端,我想提供一个“限流管理”接口的并不是难事儿。

针对指定的用户限流

这里演示的方法是,针对接口的限流。有时候,也有一些特殊的需求,需要“针对不同”的用户来做限流。打个比方。针对A用户,允许有他1分钟请求20次接口,针对B用户,允许他1分钟请求10次接口。 这个其实也简单,只需要修改一下上面的两个限制key,在key中添加用户的唯一标识(例如:ID)

request_limit_config    "/api2:{userId}" : {"limit": 10, "time": 1, "timeUnit": "SECONDS"}

request_limit:{userId}:/api  1

在拦截器中获取到用户的ID,加上用户ID进行检索和判断,就可以完成针对用户的限流。

Restful 接口的问题

@GetMapping("/user/{id}")  // restful的检索接口,往往把ID信息放在了URI中

这就会导致上面的代码有问题,因为这里采用的是根据URI来完成的限流操作。检索不同ID的用户,会导致URI不同。 解决办法我认为也很简单。那就不要使用URI,可以通过 自定义注解,方式,不同的接口,定义不同的唯一标识。在拦截器中获取到注解,读取到唯一的编码,代替原来的URI,即可。

到此这篇关于SpingBoot中使用Redis对接口进行限流的实现的文章就介绍到这了,更多相关SpingBoot Redis接口限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • LinkedList学习示例模拟堆栈与队列数据结构

    LinkedList学习示例模拟堆栈与队列数据结构

    这篇文章主要介绍了LinkedList学习示例,模拟一个堆栈与队列数据结构,大家参考使用吧
    2014-01-01
  • Swagger/Knife4j文档注解不更新问题的常见解决方案

    Swagger/Knife4j文档注解不更新问题的常见解决方案

    在日常开发中,很多同学都会遇到明明改了 DTO 的 @Schema、@ApiModelProperty 注解,但打开 doc.html 或 swagger-ui 时,文档就是不更新,尤其是当 请求/响应对象用到了内部类(nested static class) 时,所以本文就把常见原因和解决方案总结出来,需要的朋友可以参考下
    2025-09-09
  • Spring事务annotation原理详解

    Spring事务annotation原理详解

    这篇文章主要介绍了Spring事务annotation原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • 利用Spring Boot操作MongoDB的方法教程

    利用Spring Boot操作MongoDB的方法教程

    mongodb是最早热门非关系数据库的之一,使用也比较普遍,一般会用做离线数据分析来使用,放到内网的居多,下面这篇文章主要给大家介绍了利用Spring Boot操作MongoDB的方法教程,需要的朋友可以参考下
    2017-05-05
  • 使用@NonNull注解遇到的小问题及解决

    使用@NonNull注解遇到的小问题及解决

    这篇文章主要介绍了使用@NonNull注解遇到的小问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • springboot整合企微webhook机器人发送消息提醒

    springboot整合企微webhook机器人发送消息提醒

    这篇文章主要为大家介绍了springboot整合企微webhook机器人发送消息提醒,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • Spring集成Seata方式(案例演示)

    Spring集成Seata方式(案例演示)

    这篇文章主要介绍了Spring集成Seata方式,本案例使用Seata-All演示,结合实例代码给大家讲解的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-08-08
  • Java序列化问题:“Serialized class has not implement Serializable interface”错误的解决方法

    Java序列化问题:“Serialized class has not impl

    在Java开发中,序列化(Serialization)是一个常见的操作,尤其是在分布式系统、网络通信或数据持久化场景中,然而,序列化过程中可能会遇到各种问题,其中最常见的一个错误是Serialized class has not implement Serializable interface,本文给大家介绍了相关的解决方法
    2025-02-02
  • 文件路径正确,报java.io.FileNotFoundException异常的原因及解决办法

    文件路径正确,报java.io.FileNotFoundException异常的原因及解决办法

    这篇文章主要介绍了文件路径正确,报java.io.FileNotFoundException异常的原因及解决办法的相关资料,需要的朋友可以参考下
    2016-04-04
  • Java垃圾回收finalize()作用详解

    Java垃圾回收finalize()作用详解

    这篇文章主要为大家详细介绍了Java垃圾回收finalize()作用,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2015-09-09

最新评论