API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter

  这篇是API限流器这个系列的终章,就是讲述如何在Spring Cloud 微服务开发中应用我发明的先进限流器。

本项目的代码地址为:https://github.com/tangaiyun/spring-cloud-microservice-ratelimiter

限流器的代码地址为:https://github.com/tangaiyun/RedisRateLimiter

开篇明义,基本思路如下:

1. 定义一个annotation - RedisLimiter

2. 在RestController 中有URL Mapping 的方法上应用RedisLimiter注解

3. 定义一个拦截器 - RateLimitCheckInterceptor,继承org.springframework.web.servlet.HandlerInterceptor

4. 定义一个WebMvcConfigurerAdapter,把上面的RateLimitCheckInterceptor启用,拦截所有的URL请求

5. 定义一个RedisRateLimiterFactory,这个工厂对RedisLimiter对象做了一些缓存。


项目的整个代码结构如图所示:

API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter_第1张图片

基本的调用时序图如下:

API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter_第2张图片


下面我将就一些重要的类和配置逐一讲解:

1. 首先是application.yml

这个文件是Spring Cloud 微服务的重点配置文件,内容为:

server:
  port: 8080                           #微服务端口
spring:
  application:
    name: ratelimitersample            #微服务名称
  jpa:                                 #JPA的配置信息
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: none
  datasource:                           # 指定数据源
    platform: h2                        # 指定数据源类型
    schema: classpath:schema.sql        # 指定h2数据库的建表脚本
    data: classpath:data.sql            # 指定h2数据库的数据脚本
  redis:                                # Redis配置信息   
    host: 192.168.0.135
    port: 6379
    password: foobared
    timeout: 2000
    pool:                               #Jedis Pool的配置信息
      maxIdle: 300
      minIdle: 100
      max-wait: -1
      max-active: 600
logging:                                # 配置日志级别,让hibernate打印出执行的SQL
  level:
    root: INFO
    org.hibernate: INFO
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
    org.hibernate.type.descriptor.sql.BasicExtractor: TRACE                                                        
eureka:                                 #eureka的配置信息
  client:
    healthcheck:                        #打开服务健康检查
      enabled: true
    serviceUrl:
      defaultZone: http://192.168.0.118:8761/eureka/
  instance:                             #服务实例细信息
    app-group-name: product
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}
    lease-expiration-duration-in-seconds: 10
    lease-renewal-interval-in-seconds: 5
    

  

2. RateLimiter自定义annotation

package com.tay.ratelimitersample.ratelimiter;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface RateLimiter {

	public enum Base {
		General, //统一控制 
		IP, 	 //按IP限制
		User     //按用户控制
	};

	Base base();
	
	String path() default "";
	
	TimeUnit timeUnit() default TimeUnit.SECONDS;

	int permits();
}

这个注解有4个属性,第一个为base,意思是限流的基准是什么,内置的枚举类型支持三种基准,统一控制、基于IP控制、基于用户控制。

第二属性为path, 一般来说这个不用配置,但是如果你的RestController方法映射的URL上带有URL参数,类似@GetMapping("/testforid/{id}") 这种,这时候这个path配置就很有必要了,本例中,你可以将path配置为path="/testforid"。

第三个属性为timeUnit, 限流的时间单位,支持TimeUnit.SECONDS,TimeUnit.MINUTES,TimeUnit.HOURS,TimeUnit.DAYS, 即秒,分,小时,天

第四个属性为permits,意思是单位时间允许访问的次数限制。

3. RedisRateLimiter, 此类为核心类,算法原理在此系列文章二中做了详细讲解,本文中略过。

4. RedisRateLimiterFactory, RedisRateLimiter的工厂类,主要是负责创建RedisRateLimiter对象,缓存RedisRateLimiter对象。

package com.tay.ratelimitersample.ratelimiter;

import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import redis.clients.jedis.JedisPool;

@Component
public class RedisRateLimiterFactory {
    
    @Autowired
    private JedisPool jedisPool;
    private final WeakHashMap limiterMap = new WeakHashMap();
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  
    public JedisPool getJedisPool() {
        return jedisPool;
    }

    public void setJedisPool(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }
    
    public RedisRateLimiter get(String keyPrefix, TimeUnit timeUnit, int permits) {
        RedisRateLimiter redisRateLimiter = null;
        try {
            lock.readLock().lock();
            if(limiterMap.containsKey(keyPrefix)) {
                redisRateLimiter = limiterMap.get(keyPrefix);
            }
        }
        finally {
            lock.readLock().unlock();  
        }
        
        if(redisRateLimiter == null) {
            try {
                lock.writeLock().lock();
                if(limiterMap.containsKey(keyPrefix)) {
                    redisRateLimiter = limiterMap.get(keyPrefix);
                }
                if(redisRateLimiter == null) {
                    redisRateLimiter = new RedisRateLimiter(jedisPool, timeUnit, permits);
                    limiterMap.put(keyPrefix, redisRateLimiter);
                }
            }
            finally {
                lock.writeLock().unlock();
            }
        }
        return redisRateLimiter;
    }
}


注意这里我是用WeakHashMap作为RedisRateLimiter缓存容器的,是为了垃圾收集器能回收长期没有使用的RedisRateeLimiter对象,防止内存泄漏


5. RedisConfig, Redis配置信息读取类,并在其中声明了一个RedisRateLimiterFactory Bean.

package com.tay.ratelimitersample.ratelimiter;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedisConfig {

	@Value("${spring.redis.host}")
	private String host;

	@Value("${spring.redis.port}")
	private int port;

	@Value("${spring.redis.password}")
	private String password;

	@Value("${spring.redis.timeout}")
	private int timeout;

	@Value("${spring.redis.pool.maxIdle}")
	private int maxIdle;

	@Value("${spring.redis.pool.minIdle}")
	private int minIdle;

	@Value("${spring.redis.pool.max-wait}")
	private long maxWaitMillis;

	@Value("${spring.redis.pool.max-active}")
	private int maxActive;

	@Bean
	public JedisPool redisPoolFactory() {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxIdle(maxIdle);
		jedisPoolConfig.setMinIdle(minIdle);
		jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
		jedisPoolConfig.setMaxTotal(maxActive);
		JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
		return jedisPool;
	}
}

6. RateLimitCheckInterceptor类,此类为Spring cloud集成的关键类。

package com.tay.ratelimitersample.ratelimiter;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class RateLimitCheckInterceptor implements HandlerInterceptor {
	
	private static final String[] IP_HEADER_KYES = { 
			"X-Forwarded-For", 
			"X-Real-IP", 
			"Proxy-Client-IP",
			"WL-Proxy-Client-IP", 
			"HTTP_CLIENT_IP", 
			"HTTP_X_FORWARDED_FOR" };
	
	private static final String USER_TOKEN_KEY = "UserToken";
	
	@Autowired
	private RedisRateLimiterFactory redisRateLimiterFactory;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		if (!(handler instanceof HandlerMethod)) {
			return true;
		}
		boolean isSuccess = true;
		HandlerMethod handlerMethod = (HandlerMethod) handler;
		Method method = handlerMethod.getMethod();
		if (method.isAnnotationPresent(RateLimiter.class)) {
			RateLimiter rateLimiterAnnotation = method.getAnnotation(RateLimiter.class);
			int permits = rateLimiterAnnotation.permits();
			TimeUnit timeUnit = rateLimiterAnnotation.timeUnit();
			String path = rateLimiterAnnotation.path();
			if ("".equals(path)) {
				path = request.getRequestURI();
			}

			if (rateLimiterAnnotation.base() == RateLimiter.Base.General) {
				String rateLimiterKey = path;
				RedisRateLimiter redisRatelimiter = redisRateLimiterFactory.get(path, timeUnit,
						permits);
				isSuccess = rateCheck(redisRatelimiter, rateLimiterKey, response);
			} else if (rateLimiterAnnotation.base() == RateLimiter.Base.IP) {
				String ip = getIP(request);
				if (ip != null) {
					String rateLimiterKey = path + ":" + ip;
					RedisRateLimiter redisRatelimiter = redisRateLimiterFactory.get(rateLimiterKey, timeUnit,
							permits);
					isSuccess =	rateCheck(redisRatelimiter, rateLimiterKey, response);
				}
			} else if (rateLimiterAnnotation.base() == RateLimiter.Base.User) {
				String userToken = getUserToken(request);
				if (userToken != null) {
					String rateLimiterKey = path + ":" + userToken;
					RedisRateLimiter redisRatelimiter = redisRateLimiterFactory.get(rateLimiterKey, timeUnit,
							permits);
					isSuccess =rateCheck(redisRatelimiter, rateLimiterKey, response);
				}
			}
			
		}
		return isSuccess;
	}

	
	
	private boolean rateCheck(RedisRateLimiter redisRatelimiter, String keyPrefix, HttpServletResponse response)
			throws Exception {
		if (!redisRatelimiter.acquire(keyPrefix)) {
			response.setStatus(HttpStatus.FORBIDDEN.value());
			response.getWriter().print("Access denied because of exceeding access rate");
			return false;
		}
		return true;
	}
	private String getIP(HttpServletRequest request) {
		for (String ipHeaderKey : IP_HEADER_KYES) {
			String ip = request.getHeader(ipHeaderKey);
			if (ip != null && ip.length() != 0 && (!"unknown".equalsIgnoreCase(ip))) {
				return ip;
			}
			else {
				return request.getRemoteHost();
			}
		}
		return null;
	}
	
	private String getUserToken(HttpServletRequest request) {
		String userToken = request.getHeader(USER_TOKEN_KEY);
		return userToken;
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
	}
	
}

此类尤为关键,当我们api限流是基于IP或用户的话,那么我们有一些约定:

基于IP限流的话,则http request的中header里面必须带有ip信息,ip的header支持

            "X-Forwarded-For",

            "X-Real-IP",
            "Proxy-Client-IP",
            "WL-Proxy-Client-IP",
            "HTTP_CLIENT_IP",
            "HTTP_X_FORWARDED_FOR"

特别注意,现在流行会把Nginx作为分发服务器,则Nginx forward正式客户端请求到下游微服务时,务必要把客户的真实IP塞入到header中往下传递,需要在Nginx server配置中加入

proxy_set_header    X-Real-IP        $remote_addr;
基于用户限流的话,本案约定用户token的header名称为:UserToken


7. WebMvcConfigurer, 此类的作用即将拦截器RateLimitCheckInterceptor对所有的API访问生效

package com.tay.ratelimitersample.ratelimiter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
	@Autowired
	private RateLimitCheckInterceptor rateLimitCheckInterceptor;
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(rateLimitCheckInterceptor).addPathPatterns("/**");
		super.addInterceptors(registry);
	}
}

地址patterns指定为"/**",即对所有请求生效。


8. RestController

package com.tay.ratelimitersample.controller;

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.tay.ratelimitersample.entity.User;
import com.tay.ratelimitersample.ratelimiter.RateLimiter;
import com.tay.ratelimitersample.repository.UserRepository;

@RestController
@RequestMapping("/api")
public class UserController {
  @Autowired
  private UserRepository userRepository;

  @GetMapping("/{id}")
  public User findById(@PathVariable Long id, @RequestHeader HttpHeaders headers) {
	  
    User findOne = this.userRepository.findOne(id);
  
    return findOne;
  }
  
  @RateLimiter(base = RateLimiter.Base.General, permits = 2, timeUnit = TimeUnit.MINUTES)
  @GetMapping("/test")
  public String test() {
	  return "test!";
  }
  
  @RateLimiter(base = RateLimiter.Base.IP, path="/testforid", permits = 4, timeUnit = TimeUnit.MINUTES)
  @GetMapping("/testforid/{id}")
  public String testforid(@PathVariable Long id) {
	  return "test! " + id;
  }

}

其中,/api/test限流是对所有请求限定为1分钟内仅限2次。 对于/api/testforid/{id}, 是针对于每个请求ip,限定为每分钟4次,注意,这里跟上面一个配置有点小区别,就是指定了path, 因为这里的url中带了PathVariable, 但实际上我们限流时并不关心具体的PathVariable是什么,所以要指定path为/testforid, 就是所有请求都会忽略{id}, 都归结到/testforid请求。


8. 运行环境, 本例运行是需要redis服务,和一个Eureka服务。如果你不想启动Eureka亦可,把ProviderUserApplication类的注解@EnableDiscoveryClient去掉,并把application.yml中的关于Eureka的配置去掉:

eureka:
  client:
    healthcheck:
      enabled: true
    serviceUrl:
      defaultZone: http://192.168.0.118:8761/eureka/
  instance:
    app-group-name: product
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}
    lease-expiration-duration-in-seconds: 10
    lease-renewal-interval-in-seconds: 5


9.测试

测试工具firefox上的插件RESTED Client和Redis Desktop Manager

a. 通过RESTED client 访问  http://192.168.0.118:8080/api/testforid/1,访问成功,则:

API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter_第3张图片

连续点击访问,达到访问频率限制,则:

API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter_第4张图片

观察redis中数据:

API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter_第5张图片


你可能感兴趣的:(API 限流器(三) 在Spring Cloud 微服务体系中集成RedisRateLimiter)