hash redis springboot_在SpingBoot中使用Redis对接口进行限流

本文介绍了一个使用Redis实现的接口限流方案,通过在一定时间内限制接口的请求次数来防止过度负载。利用Redis的Hash存储接口限流配置,并通过increment操作线程安全地增加请求计数。当达到预设限制时,返回异常信息。此外,文章还展示了如何通过SpringBoot集成Redis,并提供了动态修改限流配置的可能性,以及针对不同用户实现限流的扩展思路。
摘要由CSDN通过智能技术生成

一个基于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 {}

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;@Configurationpublic class RedisConfiguration {@Beanpublic 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;}@Overridepublic 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);@Autowiredprivate ObjectRedisTemplate objectRedisTemplate;@Overridepublic 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 {@GetMappingpublic 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;@Configurationpublic class WebMvcConfiguration implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(this.requestLimitInterceptor()).addPathPatterns("/test");}@Beanpublic RequestLimitInterceptor requestLimitInterceptor() {return new RequestLimitInterceptor();}}

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

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

使用浏览器演示

3535d4d61d6a326b9b20f9f0b0511e32.gif

最后一些问题

怎么灵活的配置

都写到这个份儿上了,如果熟悉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,即可。


首发:https://springboot.io/t/topic/2383

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值