1. How do I set the frequency limit per second, per minute, or per hour for the same IP address to access the same interface

No more words, go straight to work, first write an annotation class


import java.lang.annotation.*;

/** * Interface traffic limiting *@author rs
 *
 */
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitLimit {
    // Identifies the limit of access times within a specified SEC period
    int limit(a) default 5;  
    // Identify the time range
    long sec(a) default 5;
}
Copy the code

The reason for using an annotation is that we use an interceptor to check whether the annotation exists on a request interface before the request is processed, and if so, get the number of accesses and the time period (e.g., only once in 1s). Next, let’s write an interceptor

import org.test.annotation.VisitLimit;
import org.test.exception.BusinessException;
import org.test.redis.RedisCache;
import org.test.service.redis.RedisService;
import org.test.util.IPUtils;
import org.test.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

@Component
public class VisitLimitInterceptor extends HandlerInterceptorAdapter {
	 
    @Autowired
	private RedisUtils redisService;

    /** * is called before processing the request@param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if(! method.isAnnotationPresent(VisitLimit.class)) {return true;
            }
            VisitLimit accessLimit = method.getAnnotation(VisitLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int limit = accessLimit.limit();
            long sec = accessLimit.sec();
            String key = IPUtils.getIpAddr(request) + request.getRequestURI();
            Integer maxLimit =null;
            Object value =redisService.get(key);
            if(value! =null && !value.equals("")) {
            	maxLimit = Integer.valueOf(String.valueOf(value));
            }
            if (maxLimit == null) {
            	redisService.set(key, "1", sec);
            } else if (maxLimit < limit) {
            	Integer i = maxLimit+1;
            	redisService.set(key, i.toString(), sec);
            } else {
// output(response, "Too many requests!") );
// return false;
                throw new BusinessException(500."Too many requests!"); }}return true;
    }
 
    @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 {}Copy the code

Redis key (IP+URL) records the number of times an IP accesses an interface, plus an expiration time. The expiration time is the value we assign to the annotation.

Part of the code for Redis is also posted here

@Service
public class RedisUtils {
    @Resource
    private RedisTemplate redisTemplate;

  /** * Write cache set aging time **@param key
     * @param value
     * @paramExpireTime Specifies the valid time, expressed in seconds@return* /
    public boolean set(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        returnresult; }}Copy the code

How to get the user’s real IP??

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

/**
 * IP Utils
 * @author rs
 *
 */
public class IPUtils {
	private static Logger logger = LoggerFactory.getLogger(IPUtils.class);

	If you use reverse proxy software such as Nginx, you cannot obtain the IP address by requesting. GetRemoteAddr () * If you use reverse proxy software, the value of x-forwarded-for is not one, but a list of IP addresses. Forwarded-for indicates the real IP address (*/). If this parameter is displayed, it carries the first valid IP address that is not unknown
	public static String getIpAddr(HttpServletRequest request) {
    	String ip = null;
        try {
            ip = request.getHeader("x-forwarded-for");
	         if(ip ! =null&& ip.length() ! =0&&!"unknown".equalsIgnoreCase(ip)) {
	             // After multiple reverse proxies, there will be multiple IP values, the first IP is the real IP
	             if( ip.indexOf(",")! = -1 ){
	                 ip = ip.split(",") [0]; }}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
	             ip = request.getHeader("Proxy-Client-IP");
	         }
	         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
	             ip = request.getHeader("WL-Proxy-Client-IP");
	         }
	         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
	             ip = request.getHeader("HTTP_CLIENT_IP");
	         }
	         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
	             ip = request.getHeader("HTTP_X_FORWARDED_FOR");
	         }
	         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
	             ip = request.getHeader("X-Real-IP");
	         }
	         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); }}catch (Exception e) {
        	logger.error("IPUtils ERROR ", e);
        }
        
        returnip; }}Copy the code

Let’s use it formally

@VisitLimit(limit = 1, sec = 1)
@RequestMapping(value = "/close", method = RequestMethod.POST)
Copy the code

This is not a good way to deal with sudden requests, need to smooth this kind of situation, such as 200ms to handle a request, the next to the token bucket!

Second, the practical introduction of token bucket

2.1 Let’s summarize first. Let’s distinguish when to use token bucket and when to use leaky bucket

  • Token bucket: Produce one token and consume one token
  • Leaky bucket: Handle large flow, and smooth processing at a fixed rate

Scenario: Geteway Gateway

Bucket4j is implemented based on the token bucket algorithm

package org.test.gateway.filter.limit;

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import reactor.core.publisher.Mono;

@Component
public class RateLimitByIpFilter implements GatewayFilter.Ordered {

	private final static Logger logger = LoggerFactory.getLogger(RateLimitByIpFilter.class);

	private int capacity;

	private int refillTokens;

	private Duration refillDuration;

	private static final Map<String, Bucket> CACHE = new ConcurrentHashMap<>();

	public RateLimitByIpFilter(a) {}public RateLimitByIpFilter(int capacity, int refillTokens, Duration refillDuration) {
		this.capacity = capacity;
		this.refillTokens = refillTokens;
		this.refillDuration = refillDuration;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		String ip = getIpAddr(exchange.getRequest());
		if (ip.indexOf("192.168") != -1 || ip.indexOf("172.31.202") != -1) {
			return chain.filter(exchange);
		}
		Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket());
		logger.info(Frequency limited visiting IP address: + ip + ", number of available tokens:" + bucket.getAvailableTokens());
		if (bucket.tryConsume(1)) {
			return chain.filter(exchange);
		} else {
			// Invalid (exceeds the current limit)
			ServerHttpResponse response = exchange.getResponse();
			/ / set the headers
			HttpHeaders httpHeaders = response.getHeaders();
			httpHeaders.add("Content-Type"."application/json; charset=UTF-8");
			httpHeaders.add("Cache-Control"."no-store, no-cache, must-revalidate, max-age=0");
			/ / set the body
			String warningStr = "{\"code\":\"500\",\"message\":\" over stream \"}";
			DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
			returnresponse.writeWith(Mono.just(bodyDataBuffer)); }}@Override
	public int getOrder(a) {
		return -1000;
	}

	private Bucket createNewBucket(a) {
		Refill refill = Refill.of(refillTokens, refillDuration);
		Bandwidth limit = Bandwidth.classic(capacity, refill);
		return Bucket4j.builder().addLimit(limit).build();
	}

	public static String getIpAddr(ServerHttpRequest request) {
		String ip = "";
		String str = request.getHeaders().getFirst("x-forwarded-for");
		if (StringUtils.isEmpty(str)) {
			ip = request.getRemoteAddress().getAddress().getHostAddress();
			logger.info(The IP addresses obtained through the address mode are as follows: + ip);
			return ip;
		} else {
			String[] ips = str.split(",");
			for (String s : ips) {
				if (s.indexOf("192.168") != -1 || s.indexOf("172.31.202") != -1) {
					continue;
				}
				ip = ip + s + ",";
			}
			ip = ip.substring(0, ip.length() - 1);
			logger.info(If this IP address is forwarded by X-forwarded-for, and filtered out from the Intranet, it is displayed as follows: + ip);
			if (ip.indexOf(",") != -1) {
				ip = ip.substring(0, ip.indexOf(","));
				return ip.trim();
			}
			returnip.trim(); }}}Copy the code

Configure route: all API interfaces of test-auth /** are routed to the test-Auth service

@SpringBootApplication
public class GateWayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GateWayApplication.class, args);
	}

	@Bean
	public RouteLocator routeLocator(RouteLocatorBuilder builder) {
		return builder.routes()
				// Certification center
				.route(r -> r.path("/TEST-AUTH/**")
						.filters(f -> f.stripPrefix(1).filter(new RateLimitByIpFilter(1.1, Duration.ofSeconds(1))))
						.uri("lb://TEST-AUTH").id("TEST-AUTH")) .build(); }}Copy the code

3. Distributed flow limiting (here excerpted from JINGdong’s snap up business)

Use the Redis+Lua method to achieve

local key = "rate.limit:". KEYS[1Local limit = tonumber(ARGV[1Local current = tonumber(redis.call())'get', key) or "0")
if current + 1> limit then -- if the current limit is exceededreturn 0
else-- Number of requests +1, and set the1Second expired redis. Call ("INCRBY", key,"1")
   redis.call("expire", key,"1")
   return  1
end
Copy the code
public static boolean accquire(a) throws IOException, URISyntaxException {
    Jedis jedis = new Jedis("127.0.0.1");
    File luaFile = new File(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath() + "limit.lua");
    String luaScript = FileUtils.readFileToString(luaFile);

    String key = "ip:" + System.currentTimeMillis()/1000; / / the second
    String limit = "5"; // Maximum limit
    List<String> keys = new ArrayList<String>();
    keys.add(key);
    List<String> args = new ArrayList<String>();
    args.add(limit);
    Long result = (Long)(jedis.eval(luaScript, keys, args)); // Execute the lua script, passing in the parameters
    return result == 1;
}
Copy the code

Redis k = rate-limit: IP: current second V: 5

Of course, you can also use Redisson to achieve oh, MY Github has test cases and pressure test scripts about Redisson welcome to connect three.

Welcome to correct communication oh!!

Popular recommendations:

  • Add the two numbers together

  • ThreadPoolExecutor thread pool

  • Add JVM parameters to microservices as they prepare to go live

  • To get into a big factory, you have to know the BASICS of CPU caching

At the end of the article, I have compiled a recent interview data “Java Interview Customs Manual”, covering Java core technology, JVM, Java concurrency, SSM, microservices, database, data structure and so on. You can obtain it from GitHub github.com/Tingyu-Note… More to come.