限流實戰:Redis+Lua實現分佈式限流

目錄

一 引入依賴

二 配置文件

2.1 Redis 配置 

2.2 Lua 腳本文件

三 Bean 配置

四 限流注解

五 攔截器限流

5.1 自定義攔截器,在攔截器中實現限流

5.2 實現 WebMvcConfigurer 添加自定義攔截器 

六 controller 類實現

七 限流測試

八 參考文檔


一 引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

二 配置文件

2.1 Redis 配置 

application.yml 配置:

spring:
  redis:
    host: 127.0.0.1
    database: 0
    port: 6379
    timeout: 1000
    jedis:
      pool:
        max-active: 256
        max-idle: 8
        min-idle: 1
        max-wait: 100

2.2 Lua 腳本文件

rateLimiter.lua 配置:

local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
   return 0
else  --請求數+1,並設置2秒過期
   redis.call("INCRBY", key,"1")
   redis.call("expire", key,"2")
   return current + 1
end

三 Bean 配置

package com.sb.springbootdistributedrateLimiter.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;

import java.io.Serializable;

@Configuration
public class CommentConfig {
    /**
     * 讀取限流腳本
     *
     * @return
     */
    @Bean
    public DefaultRedisScript<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

    /**
     * RedisTemplate
     *
     * @return
     */
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

四 限流注解

package com.sb.springbootdistributedrateLimiter.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedRateLimiterAnnotation {
    /**
     * 限流唯一標示
     *
     * @return
     */
    String key() default "";

    /**
     * 限流時間
     *
     * @return
     */
    int time();

    /**
     * 限流次數
     *
     * @return
     */
    int count();
}

五 攔截器限流

5.1 自定義攔截器,在攔截器中實現限流

DistributedRateLimiterInterceptor 類:

package com.sb.springbootdistributedrateLimiter.interceptor;

import com.sb.springbootdistributedrateLimiter.annotation.DistributedRateLimiterAnnotation;
import com.sb.springbootdistributedrateLimiter.utils.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class DistributedRateLimiterInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    @Autowired
    private DefaultRedisScript<Number> redisluaScript;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        DistributedRateLimiterAnnotation distributedRateLimiterAnnotation = handlerMethod.getMethod().getAnnotation(DistributedRateLimiterAnnotation.class);
        if(distributedRateLimiterAnnotation == null) {
            return true;
        }

        String ip = IpUtil.getIp(request);
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(ip).append("-")
                .append(request.getRequestURL()).append("-")
                .append(distributedRateLimiterAnnotation.key());

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

        Number number = redisTemplate.execute(redisluaScript, keys, distributedRateLimiterAnnotation.count(), distributedRateLimiterAnnotation.time());

        if (number != null && number.intValue() != 0 && number.intValue() <= distributedRateLimiterAnnotation.count()) {
            log.info("第{}次請求成功", number.toString());
            return true;
        } else {
            log.info("限流中");
            return false;
        }
    }

    @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 {
    }
}

IpUtil 類:

package com.sb.springbootdistributedrateLimiter.utils;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class IpUtil {
    /**
     * 獲取用戶真實IP地址,不使用request.getRemoteAddr();的原因是有可能用戶使用了代理軟件方式避免真實IP地址,
     * <p>
     * 可是,如果通過了多級反向代理的話,X-Forwarded-For的值並不止一個,而是一串IP值,究竟哪個纔是真正的用戶端的真實IP呢? 答案是取X-Forwarded-For中第一個非unknown的有效IP字符串。
     * <p>
     * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100
     * <p>
     * 用戶真實IP爲: 192.168.1.110
     *
     * @param request
     * @return
     */
    public static String getIp(HttpServletRequest request) {

        String ip = request.getHeader("x-forwarded-for");

        if (StringUtils.isNotEmpty(ip)) {
            ip = ip.replaceAll(" ", "").replaceAll("unknown", "");
            if (ip.indexOf(",") > 0) {
                ip = ip.substring(0, ip.indexOf(","));
            }
        }
        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.getRemoteAddr();
        }

        if (ip != null && isIP(ip)) {
            return ip;
        }
        return null;
    }


    public static boolean isIP(String addr) {
        if (addr.length() < 7 || addr.length() > 15 || "".equals(addr)) {
            return false;
        }
        /**
         * 判斷IP格式和範圍
         */
        String rexp = "^(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])$";

        Pattern pat = Pattern.compile(rexp);

        Matcher mat = pat.matcher(addr);

        boolean ipAddress = mat.find();

        return ipAddress;
    }

}

5.2 實現 WebMvcConfigurer 添加自定義攔截器 

package com.sb.springbootdistributedrateLimiter.config;
import com.sb.springbootdistributedrateLimiter.interceptor.DistributedRateLimiterInterceptor;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
 
@SpringBootConfiguration
public class WebConfiguration implements WebMvcConfigurer {
 
    @Resource
    private DistributedRateLimiterInterceptor distributedRateLimiterInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(distributedRateLimiterInterceptor).addPathPatterns("/**");
    }
}

六 controller 類實現

package com.sb.springbootdistributedrateLimiter.controller;

import com.sb.springbootdistributedrateLimiter.annotation.DistributedRateLimiterAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/api")
public class RateLimiterController {

    // 10 秒中,可以訪問10次
    @DistributedRateLimiterAnnotation(key = "test", time = 10, count = 10)
    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public void rateLimiterTest() throws Exception {
        //業務邏輯;
    }
}

七 限流測試

通過 jmeter 發送 20 QPS 請求接口得到如下結果:

八 參考文檔

https://www.iteye.com/blog/jinnianshilongnian-2305117

https://segmentfault.com/a/1190000016042927

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章