[Reproduced please indicate the source] :Juejin. Cn/post / 684490…

The key of distributed traffic limiting is to make the traffic limiting service atomized, and the solution can be realized by using Redis + Lua or Nginx + Lua technology, which can achieve high concurrency and high performance.

First of all, we use Redis + Lua to realize the number of requests to limit the flow of an interface in the time window. After realizing this function, it can be transformed to limit the total number of concurrent/requests and limit the total number of resources. Lua, itself a programming language, can also be used to implement complex token bucket or leaky bucket algorithms. Because the operation is in a Lua script (equivalent to atomic operation) and because Redis is a single-threaded model, it is thread-safe.

Lua scripts have the following advantages over Redis transactions

  • Reduce network overhead: code that doesn’t use Lua needs to send multiple requests to Redis, while scripts need only one, reducing network traffic;
  • Atomic operations: Redis executes the entire script as a single atom without worrying about concurrency and therefore transactions;
  • Reuse: Scripts are stored permanently in Redis and can be used by other clients.

The following uses the SpringBoot project for introduction.

Prepare the Lua script

req_ratelimit.lua

local key = "req.rate.limit:". KEYS[1] -- limiting KEYlocal limitCount = tonumber(ARGV[1]) -- limit sizelocal limitTime = tonumber(ARGV[2]) -- limiting Timelocal current = tonumber(redis.call('get', key) or "0")
if current + 1 > limitCount then-- If the traffic limit is exceededreturn 0
else-- Request count +1 and set expiration to 1 second redis. Call ("INCRBY", key,"1")
    redis.call("expire", key,limitTime)
    return current + 1
end
Copy the code
  • We get the key argument passed in by KEYS[1]
  • Get the limit argument passed in with ARGV[1]
  • The redis. Call method returns 0 if the get and key values from the cache are nil
  • Then determine whether the value recorded in the cache will be greater than the limit size. If the value is greater than the limit size, return 0
  • If not, the key’s cache value +1 is set to expire after 1 second, and the cache value +1 is returned

Preparing a Java project

Pom. XML to join
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId>  </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies>Copy the code

Redis configuration

Spring. Redis. Host = 127.0.0.1 spring. Redis. Port = 6379 spring. Redis. Password = spring. Redis. Database = 0# maximum number of connections in the pool (use negative values to indicate no limit)
spring.redis.jedis.pool.max-active=20
Maximum connection pool blocking wait time (negative value indicates no limit)
spring.redis.jedis.pool.max-wait=-1
The maximum number of free connections in the connection pool
spring.redis.jedis.pool.max-idle=10
Minimum free connection in connection pool
spring.redis.jedis.pool.min-idle=0
Connection timeout (ms)
spring.redis.timeout=2000
Copy the code
Current limiting annotations

Annotations are intended for use in methods that require limiting traffic

@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(retentionPolicy.runtime) public @interface RateLimiter {/** * Unique identifier * @return
     */
    String key() default ""; /** * Current limiting time * @return*/ int time(); /** ** Limit the number of times * @return
     */
    int count();

}
Copy the code
Lua file configuration and RedisTemplate configuration
@Aspect
@Configuration
@Slf4j
public class RateLimiterAspect {


    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    @Autowired
    private DefaultRedisScript<Number> redisScript;

    @Around("execution(* com.sunlands.zlcx.datafix.web .. * (..) )") public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Class<? > targetClass = method.getDeclaringClass(); RateLimiter rateLimit = method.getAnnotation(RateLimiter.class);if(rateLimit ! = null) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ipAddress = getIpAddr(request); StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(ipAddress).append("-")
                    .append(targetClass.getName()).append("-")
                    .append(method.getName()).append("-")
                    .append(rateLimit.key());

            List<String> keys = Collections.singletonList(stringBuffer.toString());

            Number number = redisTemplate.execute(redisScript, keys, rateLimit.count(), rateLimit.time());

            if(number ! = null && number.intValue() ! = 0 && number.intValue() <= rateLimit.count()) { log.info("Access number: {} within traffic limiting period", number.toString());
                returnjoinPoint.proceed(); }}else {
            return joinPoint.proceed();
        }

        throw new RuntimeException("Current limiting times have been set.");
    }

    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); } // In the case of multiple proxies, the first IP address is the real IP address of the client', 'segmentationif(ipAddress ! = null && ipAddress.length() > 15) { //"* * *. * * *. * * *. * * *".length()= 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        returnipAddress; }}Copy the code
Control layer
@RestController
@Slf4j
@RequestMapping("limit")
public class RateLimiterController {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping(value = "/test")
    @RateLimiter(key = "test", time = 10, count = 1)
    public ResponseEntity<Object> test() {

        String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
        RedisAtomicInteger limitCounter = new RedisAtomicInteger("limitCounter", redisTemplate.getConnectionFactory());
        String str = date + "Number of visits:" + limitCounter.getAndIncrement();
        log.info(str);
        returnResponseEntity.ok(str); }}Copy the code

Start the project for testing

Continuous access to the url http://127.0.0.1:8090/limit/test, the effect is as follows:

Instead of throwing a RuntimeException for the sake of a simple demonstration, you can actually define a separate exception such as RateLimitException that handles this frequency-limited exception directly on top and returns it to the user in a friendly way.

[Reproduced please indicate the source] :Juejin. Cn/post / 684490…