1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot 項(xiàng)目使用 Redis 對(duì)用戶 IP 進(jìn)行接口限流

        共 17057字,需瀏覽 35分鐘

         ·

        2023-07-27 08:11

        胖虎和朋友原創(chuàng)的視頻教程有興趣的可以看看


        (文末附課程大綱)


        ??2023 最新,Java成神之路,架構(gòu)視頻(點(diǎn)擊查看)


        ??超全技術(shù)棧的Java入門+進(jìn)階+實(shí)戰(zhàn)!(點(diǎn)擊查看)


        一、思路

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

        比如要求某接口在1分鐘內(nèi)請(qǐng)求次數(shù)不超過1000次,那么應(yīng)該如何設(shè)計(jì)代碼呢?

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

        1.1 固定時(shí)間段(舊思路)

        1.1.1 思路描述

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

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

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

        基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 實(shí)現(xiàn)的前后端分離博客,包含后臺(tái)管理系統(tǒng),支持文章、分類、標(biāo)簽管理、儀表盤等功能。

        • GitHub 地址:https://github.com/weiwosuoai/WeBlog
        • Gitee 地址:https://gitee.com/AllenJiang/WeBlog
        
           

        之后,只要這個(gè)key還未過期,用戶每次訪問該接口都會(huì)導(dǎo)致value自增1次。

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

        圖片
        1.1.2 思路缺陷

        該方案的缺點(diǎn)在于,限流時(shí)間段是固定的。

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

        圖片

        圖片

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

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

        1.2 滑動(dòng)窗口(新思路)

        1.2.1 思路描述

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

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

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

        圖片

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

        圖片
        1.2.2 Redis部分的實(shí)現(xiàn)

        1)選用何種 Redis 數(shù)據(jù)結(jié)構(gòu)

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

        2)為何選擇 zSet 數(shù)據(jù)結(jié)構(gòu)

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

        添加一個(gè)zSet元素的命令如下:

        ZADD [key] [score] [member]

        它有一個(gè)關(guān)鍵的屬性score,通過它可以記錄當(dāng)前member的優(yōu)先級(jí)。

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

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

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

        ① 把特定時(shí)間間隔之前的member都刪掉。

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

        Zremrangebyscore [key] [min] [max]

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

        min = 0
        max = currentTimeMill - 5 * 1000

        相當(dāng)于把5秒之前的所有member都刪除了,只留下前5秒內(nèi)的key。

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

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

         ZCARD [key]

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

        二、代碼實(shí)現(xiàn)

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

        基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 實(shí)現(xiàn)的前后端分離博客,包含后臺(tái)管理系統(tǒng),支持文章、分類、標(biāo)簽管理、儀表盤等功能。

        • GitHub 地址:https://github.com/weiwosuoai/WeBlog
        • Gitee 地址:https://gitee.com/AllenJiang/WeBlog
        
           

        2.1 固定時(shí)間段思路

        使用了lua腳本。

        • 參考:https://blog.csdn.net/qq_43641418/article/details/127764462
        2.1.1 限流注解
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.METHOD)
        public @interface RateLimiter {

            /**
             * 限流時(shí)間,單位秒
             */
            int time() default 5;

            /**
             * 限流次數(shù)
             */
            int count() default 10;
        }
        2.1.2 定義lua腳本

        resources/lua下新建limit.lua

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

        關(guān)鍵代碼是limitScript()方法

        @Configuration
        public class RedisConfig {

            @Bean
            public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
                RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
                redisTemplate.setConnectionFactory(connectionFactory);
                // 使用Jackson2JsonRedisSerialize 替換默認(rèn)序列化(默認(rèn)采用的是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);
                    // 當(dāng)前流量number已超過限制,則拋出異常
                    if (number == null || number.intValue() > count) {
                     throw new RuntimeException("訪問過于頻繁,請(qǐng)稍后再試");
                    }
                    log.info("[limit] 限制請(qǐng)求數(shù)'{}',當(dāng)前請(qǐng)求數(shù)'{}',緩存key'{}'", count, number.intValue(), combineKey);
                } catch (Exception ex) {
                    ex.printStackTrace();
                    throw new RuntimeException("服務(wù)器限流異常,請(qǐng)稍候再試");
                }
            }
            
            /**
             * 把用戶IP和接口方法名拼接成 redis 的 key
             * @param point 切入點(diǎn)
             * @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 滑動(dòng)窗口思路

        2.2.1 限流注解
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.METHOD)
        public @interface RateLimiter {

            /**
             * 限流時(shí)間,單位秒
             */
            int time() default 5;

            /**
             * 限流次數(shù)
             */
            int count() default 10;
        }
        2.2.2 定義Aop切面類
        @Slf4j
        @Aspect
        @Component
        public class RateLimiterAspect {

            @Autowired
            private RedisTemplate redisTemplate;

            /**
             * 實(shí)現(xiàn)限流(新思路)
             * @param point
             * @param rateLimiter
             * @throws Throwable
             */
            @SuppressWarnings("unchecked")
            @Before("@annotation(rateLimiter)")
            public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
                // 在 {time} 秒內(nèi)僅允許訪問 {count} 次。
                int time = rateLimiter.time();
                int count = rateLimiter.count();
                // 根據(jù)用戶IP(可選)和接口方法,構(gòu)造key
                String combineKey = getCombineKey(rateLimiter.type(), point);
                
                // 限流邏輯實(shí)現(xiàn)
                ZSetOperations zSetOperations = redisTemplate.opsForZSet();
                // 記錄本次訪問的時(shí)間結(jié)點(diǎn)
                long currentMs = System.currentTimeMillis();
                zSetOperations.add(combineKey, currentMs, currentMs);
                // 這一步是為了防止member一直存在于內(nèi)存中
                redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);
                // 移除{time}秒之前的訪問記錄(滑動(dòng)窗口思想)
                zSetOperations.removeRangeByScore(combineKey, 0, currentMs - time * 1000);
                
                // 獲得當(dāng)前窗口內(nèi)的訪問記錄數(shù)
                Long currCount = zSetOperations.zCard(combineKey);
                // 限流判斷
                if (currCount > count) {
                    log.error("[limit] 限制請(qǐng)求數(shù)'{}',當(dāng)前請(qǐng)求數(shù)'{}',緩存key'{}'", count, currCount, combineKey);
                    throw new RuntimeException("訪問過于頻繁,請(qǐng)稍后再試!");
                }
            }

            /**
             * 把用戶IP和接口方法名拼接成 redis 的 key
             * @param point 切入點(diǎn)
             * @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();
            }
        }

        來源:blog.csdn.net/weixin_44213308/article/details/111151350

           

                 

        胖虎聯(lián)合兩位大佬朋友,一位是知名培訓(xùn)機(jī)構(gòu)講師和科大訊飛架構(gòu),聯(lián)合打造了《Java架構(gòu)師成長(zhǎng)之路》的視頻教程。完全對(duì)標(biāo)外面2萬左右的培訓(xùn)課程。

        除了基本的視頻教程之外,還提供了超詳細(xì)的課堂筆記,以及源碼等資料包..


        課程階段:

        1. Java核心 提升閱讀源碼的內(nèi)功心法
        2. 深入講解企業(yè)開發(fā)必備技術(shù)棧,夯實(shí)基礎(chǔ),為跳槽加薪增加籌碼
        3. 分布式架構(gòu)設(shè)計(jì)方法論。為學(xué)習(xí)分布式微服務(wù)做鋪墊
        4. 學(xué)習(xí)NetFilx公司產(chǎn)品,如Eureka、Hystrix、Zuul、Feign、Ribbon等,以及學(xué)習(xí)Spring Cloud Alibabba體系
        5. 微服務(wù)架構(gòu)下的性能優(yōu)化
        6. 中間件源碼剖析
        7. 元原生以及虛擬化技術(shù)
        8. 從0開始,項(xiàng)目實(shí)戰(zhàn) SpringCloud Alibaba電商項(xiàng)目

        點(diǎn)擊下方超鏈接查看詳情(或者點(diǎn)擊文末閱讀原文):

        (點(diǎn)擊查看)  2023年,最新Java架構(gòu)師成長(zhǎng)之路 視頻教程!

        以下是課程大綱,大家可以雙擊打開原圖查看

        瀏覽 37
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            一级做a爰片久久毛片A片下乡 | 亚洲欧美黄色电影 | 女人扒开腿让男人捅爽 | 男人的j插入女人的b | 大香蕉熟女 | 狠狠色丁香婷婷综合久久片 | 一级AAA黄片 | 国产一区一一区高清不卡 | ass日本少妇高潮pics | 亚洲精品一区中文字幕 |