亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

SpringBoot使用Redis對用戶IP進行接口限流的項目實踐

 更新時間:2023年07月28日 15:28:45   作者:莫輕言舞  
本文主要介紹了SpringBoot使用Redis對用戶IP進行接口限流,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

一、思路

使用接口限流的主要目的在于提高系統(tǒng)的穩(wěn)定性,防止接口被惡意打擊(短時間內大量請求)。

比如要求某接口在1分鐘內請求次數(shù)不超過1000次,那么應該如何設計代碼呢?

下面講兩種思路,如果想看代碼可直接翻到后面的代碼部分。

1.1 固定時間段(舊思路)

1.1.1 思路描述

該方案的思路是:使用Redis記錄固定時間段內某用戶IP訪問某接口的次數(shù),其中:

  • Redis的key:用戶IP + 接口方法名
  • Redis的value:當前接口訪問次數(shù)。

當用戶在近期內第一次訪問該接口時,向Redis中設置一個包含了用戶IP和接口方法名的key,value的值初始化為1(表示第一次訪問當前接口)。同時,設置該key的過期時間(比如為60秒)。

之后,只要這個key還未過期,用戶每次訪問該接口都會導致value自增1次。

用戶每次訪問接口前,先從Redis中拿到當前接口訪問次數(shù),如果發(fā)現(xiàn)訪問次數(shù)大于規(guī)定的次數(shù)(如超過1000次),則向用戶返回接口訪問失敗的標識。

圖片

1.1.2 思路缺陷

該方案的缺點在于,限流時間段是固定的。

比如要求某接口在1分鐘內請求次數(shù)不超過1000次,觀察以下流程:

圖片

圖片

可以發(fā)現(xiàn),00:59和01:01之間僅僅間隔了2秒,但接口卻被訪問了1000+999=1999次,是限流次數(shù)(1000次)的2倍!

所以在該方案中,限流次數(shù)的設置可能不起作用,仍然可能在短時間內造成大量訪問。

1.2 滑動窗口(新思路)

1.2.1 思路描述

為了避免出現(xiàn)方案1中由于鍵過期導致的短期訪問量增大的情況,我們可以改變一下思路,也就是把固定的時間段改成動態(tài)的:

假設某個接口在10秒內只允許訪問5次。用戶每次訪問接口時,記錄當前用戶訪問的時間點(時間戳),并計算前10秒內用戶訪問該接口的總次數(shù)。如果總次數(shù)大于限流次數(shù),則不允許用戶訪問該接口。這樣就能保證在任意時刻用戶的訪問次數(shù)不會超過1000次。

如下圖,假設用戶在0:19時間點訪問接口,經檢查其前10秒內訪問次數(shù)為5次,則允許本次訪問。

圖片

假設用戶0:20時間點訪問接口,經檢查其前10秒內訪問次數(shù)為6次(超出限流次數(shù)5次),則不允許本次訪問。

圖片

1.2.2 Redis部分的實現(xiàn)

1)選用何種 Redis 數(shù)據(jù)結構

首先是需要確定使用哪個Redis數(shù)據(jù)結構。用戶每次訪問時,需要用一個key記錄用戶訪問的時間點,而且還需要利用這些時間點進行范圍檢查。

2)為何選擇 zSet 數(shù)據(jù)結構

為了能夠實現(xiàn)范圍檢查,可以考慮使用Redis中的zSet有序集合。

添加一個zSet元素的命令如下:

ZADD?[key]?[score]?[member]

它有一個關鍵的屬性score,通過它可以記錄當前member的優(yōu)先級。

于是我們可以把score設置成用戶訪問接口的時間戳,以便于通過score進行范圍檢查。key則記錄用戶IP和接口方法名,至于member設置成什么沒有影響,一個member記錄了用戶訪問接口的時間點。因此member也可以設置成時間戳。

3)zSet 如何進行范圍檢查(檢查前幾秒的訪問次數(shù))

思路是,把特定時間間隔之前的member都刪掉,留下的member就是時間間隔之內的總訪問次數(shù)。然后統(tǒng)計當前key中的member有多少個即可。

① 把特定時間間隔之前的member都刪掉。

zSet有如下命令,用于刪除score范圍在[min~max]之間的member:

Zremrangebyscore?[key]?[min]?[max]

假設限流時間設置為5秒,當前用戶訪問接口時,獲取當前系統(tǒng)時間戳為currentTimeMill,那么刪除的score范圍可以設置為:

min = 0
max = currentTimeMill - 5 * 1000

相當于把5秒之前的所有member都刪除了,只留下前5秒內的key。

② 統(tǒng)計特定key中已存在的member有多少個。

zSet有如下命令,用于統(tǒng)計某個key的member總數(shù):

?ZCARD?[key]

統(tǒng)計的key的member總數(shù),就是當前接口已經訪問的次數(shù)。如果該數(shù)目大于限流次數(shù),則說明當前的訪問應被限流。

二、代碼實現(xiàn)

主要是使用注解 + AOP的形式實現(xiàn)。

2.1 固定時間段思路

使用了lua腳本。

參考:http://chabaoo.cn/program/293608ct1.htm

2.1.1 限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
    /**
     * 限流時間,單位秒
     */
    int time() default 5;
    /**
     * 限流次數(shù)
     */
    int count() default 10;
}

2.1.2 定義lua腳本

resources/lua下新建limit.lua

-- 獲取redis鍵
local key = KEYS[1]
-- 獲取第一個參數(shù)(次數(shù))
local count = tonumber(ARGV[1])
-- 獲取第二個參數(shù)(時間)
local time = tonumber(ARGV[2])
-- 獲取當前流量
local current = redis.call('get', key);
-- 如果current值存在,且值大于規(guī)定的次數(shù),則拒絕放行(直接返回當前流量)
if current and tonumber(current) > count then
    return tonumber(current)
end
-- 如果值小于規(guī)定次數(shù),或值不存在,則允許放行,當前流量數(shù)+1  (值不存在情況下,可以自增變?yōu)?)
current = redis.call('incr', key);
-- 如果是第一次進來,那么開始設置鍵的過期時間。
if tonumber(current) == 1 then 
    redis.call('expire', key, time);
end
-- 返回當前流量
return tonumber(current)

2.1.3 注入Lua執(zhí)行腳本

關鍵代碼是limitScript()方法

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替換默認序列化(默認采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
    /**
     * 解析lua腳本的bean
     */
    @Bean("limitScript")
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

2.1.3 定義Aop切面類

@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
 @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisScript<Long> limitScript;
 @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        String combineKey = getCombineKey(rateLimiter.type(), point);
        List<String> keys = Collections.singletonList(combineKey);
        try {
            Long number = (Long) redisTemplate.execute(limitScript, keys, count, time);
            // 當前流量number已超過限制,則拋出異常
            if (number == null || number.intValue() > count) {
             throw new RuntimeException("訪問過于頻繁,請稍后再試");
            }
            log.info("[limit] 限制請求數(shù)'{}',當前請求數(shù)'{}',緩存key'{}'", count, number.intValue(), combineKey);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException("服務器限流異常,請稍候再試");
        }
    }
    /**
     * 把用戶IP和接口方法名拼接成 redis 的 key
     * @param point 切入點
     * @return 組合key
     */
    private String getCombineKey(JoinPoint point) {
        StringBuilder sb = new StringBuilder("rate_limit:");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // keyPrefix + "-" + class + "-" + method
        return sb.append("-").append( targetClass.getName() )
                .append("-").append(method.getName()).toString();
    }
}

2.2 滑動窗口思路

2.2.1 限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
    /**
     * 限流時間,單位秒
     */
    int time() default 5;
    /**
     * 限流次數(shù)
     */
    int count() default 10;
}

2.2.2 定義Aop切面類

@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 實現(xiàn)限流(新思路)
     * @param point
     * @param rateLimiter
     * @throws Throwable
     */
    @SuppressWarnings("unchecked")
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        // 在 {time} 秒內僅允許訪問 {count} 次。
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        // 根據(jù)用戶IP(可選)和接口方法,構造key
        String combineKey = getCombineKey(rateLimiter.type(), point);
        // 限流邏輯實現(xiàn)
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        // 記錄本次訪問的時間結點
        long currentMs = System.currentTimeMillis();
        zSetOperations.add(combineKey, currentMs, currentMs);
        // 這一步是為了防止member一直存在于內存中
        redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);
        // 移除{time}秒之前的訪問記錄(滑動窗口思想)
        zSetOperations.removeRangeByScore(combineKey, 0, currentMs - time * 1000);
        // 獲得當前窗口內的訪問記錄數(shù)
        Long currCount = zSetOperations.zCard(combineKey);
        // 限流判斷
        if (currCount > count) {
            log.error("[limit] 限制請求數(shù)'{}',當前請求數(shù)'{}',緩存key'{}'", count, currCount, combineKey);
            throw new RuntimeException("訪問過于頻繁,請稍后再試!");
        }
    }
    /**
     * 把用戶IP和接口方法名拼接成 redis 的 key
     * @param point 切入點
     * @return 組合key
     */
    private String getCombineKey(JoinPoint point) {
        StringBuilder sb = new StringBuilder("rate_limit:");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // keyPrefix + "-" + class + "-" + method
        return sb.append("-").append( targetClass.getName() )
                .append("-").append(method.getName()).toString();
    }
}

到此這篇關于SpringBoot使用Redis對用戶IP進行接口限流的文章就介紹到這了,更多相關SpringBoot Redis接口限流內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Spring boot 數(shù)據(jù)源未配置異常的解決

    Spring boot 數(shù)據(jù)源未配置異常的解決

    這篇文章主要介紹了Spring boot 數(shù)據(jù)源未配置異常的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • 圖文講解IDEA中根據(jù)數(shù)據(jù)庫自動生成實體類

    圖文講解IDEA中根據(jù)數(shù)據(jù)庫自動生成實體類

    這篇文章主要以圖文講解IDEA中根據(jù)數(shù)據(jù)庫自動生成實體類,本文主要以Mysql數(shù)據(jù)庫為例,應該會對大家有所幫助,如果有錯誤的地方,還望指正
    2023-03-03
  • Java設計模式—觀察者模式詳解

    Java設計模式—觀察者模式詳解

    這篇文章主要介紹了Java設計模式—觀察者模式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-03-03
  • SpringBoot上傳和下載文件的原理解析

    SpringBoot上傳和下載文件的原理解析

    文件上傳的功能很常見,但是很多人并不清楚文件上傳后的具體原理,接下來通過本文給大家分享SpringBoot上傳和下載文件的原理解析,感興趣的朋友一起看看吧
    2021-07-07
  • shiro實現(xiàn)單點登錄(一個用戶同一時刻只能在一個地方登錄)

    shiro實現(xiàn)單點登錄(一個用戶同一時刻只能在一個地方登錄)

    這篇文章主要介紹了shiro實現(xiàn)單點登錄(一個用戶同一時刻只能在一個地方登錄)的相關資料,非常不錯,具有參考借鑒價值,感興趣的朋友一起學習吧
    2016-08-08
  • IDEA自定義常用代碼塊及自定義快捷摸板

    IDEA自定義常用代碼塊及自定義快捷摸板

    這篇文章主要介紹了IDEA自定義常用代碼塊及自定義快捷摸板的相關知識,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2020-09-09
  • 詳解Java中用于國際化的locale類

    詳解Java中用于國際化的locale類

    Java中也有用于轉換和劃分地區(qū)的國際化類java.lang.Locale,國際化在程序中設置語言和時間等時非常有用,下面我們就來詳解Java中用于國際化的locale類
    2016-06-06
  • Spring如何使用PropertyPlaceholderConfigurer讀取文件

    Spring如何使用PropertyPlaceholderConfigurer讀取文件

    這篇文章主要介紹了Spring如何使用PropertyPlaceholderConfigurer讀取文件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-12-12
  • Java 獲取上一個月的月份的正確寫法

    Java 獲取上一個月的月份的正確寫法

    這篇文章主要介紹了java獲取上一個月月份,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-09-09
  • 接口重試的7種常用方案詳細介紹

    接口重試的7種常用方案詳細介紹

    這篇文章主要為大家詳細介紹了接口重試的7種常用方案,文中的示例代碼講解詳細,具有一定的借鑒價值,有需要的小伙伴可以根據(jù)需求選擇
    2025-03-03

最新評論