SpringBoot限流攔截器(結合業務)

SpringBoot限流攔截器(結合業務)

背景

從網絡安全和系統穩定性來看,限流是非常有必要的。
一些網關,可以幫我們完成限流熔斷。但是,在某些場景,當與實際業務相結合時,網關的限流也就不那麼方便了。

1.目的

1.解決業務和限流合併的情況。
如,同一個接口,每個用戶,在一段時間(10秒)內只能請求幾次(4次)。
2.並且可以快速的調整這個限制的頻率(動態修改)

  • SpringBoot
  • Redis

2.配置關係

Redis > application.yml > 類中默認

PS:這裏是將Redis作爲一個配置中心,定時讀取。
也可以自行修改爲配置中心,如Nacos等,這裏不做展開。

注意:如果用Redis做配置中心,定時讀取的話,注意在啓動類添加@EnableScheduling註解

3.代碼

配置類

@Data
@Configuration
@ConfigurationProperties("request.limit")
public class RequestLimitProperties {
    // 配置文件,默認配置  10秒內4次

    /**
     * 判斷週期
     */
    private Integer cycle = 10;

    /**
     * 每週期請求的次數
     */
    private Integer times = 4;
}

攔截器


/**
 * 每個請求次數限制攔截器
 *
 * @author litong
 */
@Order
public class RequestLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 獲取次數
     */
    private static final String CYCLE_KEY = getCycleKey();

    /**
     * 獲取單位時間
     */
    private static final String TIMES_KEY = getTimesKey();

    /**
     * 默認判斷週期
     */
    private Integer cycle = 10;
    /**
     * 默認每週期請求的次數
     */
    private Integer times = 4;

    @Autowired
    private RequestLimitProperties requestLimitProperties;

    @PostConstruct
    public void pInit() {
        cycle = requestLimitProperties.getCycle();
        times = requestLimitProperties.getTimes();
    }

    @Scheduled(fixedDelay = 1000)
    public void fetchRequestLimitProperties() {
        //在有值,且是有效值時,認爲Redis優先級更高,更新 cycle 和 times
        Integer redisCycle = (Integer) redisTemplate.opsForValue().get(CYCLE_KEY);
        Integer redisTimes = (Integer) redisTemplate.opsForValue().get(TIMES_KEY);
        if (redisCycle != null) {
            cycle = redisCycle;
        }
        if (redisTimes != null) {
            times = redisTimes;
        }
    }

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) {
        // 獲取sid
        String token = httpServletRequest.getHeader("token");
        // 獲取請求接口路徑
        String requestURI = httpServletRequest.getRequestURI();
        if (requestURI != null) {
            String[] split = requestURI.split("/");
            if (split.length != 0) {
                requestURI = split[split.length - 1];
            }
        }
        // 獲取Redis中的緩存
        Long expire = redisTemplate.getExpire(getKey(token, requestURI), TimeUnit.SECONDS);
        if (expire == null || expire == -2) {
            // 第一次請求,記錄請求
            redisTemplate.opsForValue().set(getKey(token, requestURI), 1, cycle, TimeUnit.SECONDS);
        } else {
            // 後續請求,請求次數加1
            Integer s = (Integer) redisTemplate.opsForValue().get(getKey(token, requestURI));
            int time = 0;
            if (s != null && s != -2) {
                time = s;
            }
            int count = time + 1;
            if (count >= times + 1) {
                throw new AuthenticationException("接口請求頻率過高,sid:" + token + ",url:" + requestURI + ",當前閾值:" + times + "/" + cycle);
            }
            redisTemplate.opsForValue().set(getKey(token, requestURI), count, expire == 0 ? 1 : expire, TimeUnit.SECONDS);
        }
        return true;
    }

    private String getKey(String token, String requestURI) {
        return "REQUEST_LIMIT_REDIS:" + requestURI + ":" + token;
    }

    private static String getCycleKey() {
        return "REQUEST_LIMIT_CYCLE:";
    }

    private static String getTimesKey() {
        return "REQUEST_LIMIT_TIMES:";
    }
}

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