• 在SpingBoot中使用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,即可。


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

  • 相关阅读:
    使用IDENTITY列属性和Sequence对象
    使用OFFSET-FETCH进行数据过滤
    SQL 插入语句汇总
    建立&修改视图
    Centos 7.x 搭建 Zabbix3.4
    RDS 导出Mysqlbinlog_二进制日志
    Yac
    云服务器漏洞更新
    Centos 内存释放
    Centos 安装 Htop
  • 原文地址:https://www.cnblogs.com/kevinblandy/p/13456903.html
Copyright © 2020-2023  润新知