溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么用redis實(shí)現(xiàn)常見限流策略

發(fā)布時(shí)間:2021-06-18 10:08:01 來源:億速云 閱讀:176 作者:chen 欄目:開發(fā)技術(shù)

這篇文章主要介紹“怎么用redis實(shí)現(xiàn)常見限流策略”,在日常操作中,相信很多人在怎么用redis實(shí)現(xiàn)常見限流策略問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”怎么用redis實(shí)現(xiàn)常見限流策略”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

目錄
  • 引言

  • 固定時(shí)間窗口算法

    • 實(shí)現(xiàn)

  • 滑動(dòng)時(shí)間窗口算法

    • 實(shí)現(xiàn)

  • 漏桶算法

    • 實(shí)現(xiàn)

  • 令牌桶算法

    引言

    • 在web開發(fā)中功能是基石,除了功能以外運(yùn)維和防護(hù)就是重頭菜了。因?yàn)樵诰W(wǎng)站運(yùn)行期間可能會(huì)因?yàn)橥蝗坏脑L問量導(dǎo)致業(yè)務(wù)異常、也有可能遭受別人惡意攻擊

    • 所以我們的接口需要對(duì)流量進(jìn)行限制。俗稱的QPS也是對(duì)流量的一種描述

    • 針對(duì)限流現(xiàn)在大多應(yīng)該是令牌桶算法,因?yàn)樗鼙WC更多的吞吐量。除了令牌桶算法還有他的前身漏桶算法和簡單的計(jì)數(shù)算法

    • 下面我們來看看這四種算法

    固定時(shí)間窗口算法

    • 固定時(shí)間窗口算法也可以叫做簡單計(jì)數(shù)算法。網(wǎng)上有很多都將計(jì)數(shù)算法單獨(dú)抽離出來。但是筆者認(rèn)為計(jì)數(shù)算法是一種思想,而固定時(shí)間窗口算法是他的一種實(shí)現(xiàn)

    • 包括下面滑動(dòng)時(shí)間窗口算法也是計(jì)數(shù)算法的一種實(shí)現(xiàn)。因?yàn)橛?jì)數(shù)如果不和時(shí)間進(jìn)行綁定的話那么失去了限流的本質(zhì)了。就變成了拒絕了

    怎么用redis實(shí)現(xiàn)常見限流策略

    優(yōu)點(diǎn)

    • 在固定的時(shí)間內(nèi)出現(xiàn)流量溢出可以立即做出限流。每個(gè)時(shí)間窗口不會(huì)相互影響

    • 在時(shí)間單元內(nèi)保障系統(tǒng)的穩(wěn)定。保障的時(shí)間單元內(nèi)系統(tǒng)的吞吐量上限

    缺點(diǎn)

    • 正如圖示一樣,他的最大問題就是臨界狀態(tài)。在臨界狀態(tài)最壞情況會(huì)受到兩倍流量請(qǐng)求

    • 除了臨界的情況,還有一種是在一個(gè)單元時(shí)間窗內(nèi)前期如果很快的消耗完請(qǐng)求閾值。那么剩下的時(shí)間將會(huì)無法請(qǐng)求。這樣就會(huì)因?yàn)橐凰查g的流量導(dǎo)致一段時(shí)間內(nèi)系統(tǒng)不可用。這在互聯(lián)網(wǎng)高可用的系統(tǒng)中是不能接受的。

    實(shí)現(xiàn)

    • 好了,關(guān)于原理介紹及優(yōu)缺點(diǎn)我們已經(jīng)了解了。下面我們動(dòng)手實(shí)現(xiàn)它

    • 首先我們?cè)趯?shí)現(xiàn)這種計(jì)數(shù)時(shí),采用redis是非常好的選擇。這里我們通過redis實(shí)現(xiàn)

    controller

    @RequestMapping(value = "/start",method = RequestMethod.GET)
        public Map<string,object> start(@RequestParam Map<string, object=""> paramMap) {
            return testService.startQps(paramMap);
        }

    service

    @Override
    public Map<string, object=""> startQps(Map<string, object=""> paramMap) {
        //根據(jù)前端傳遞的qps上線
        Integer times = 100;
        if (paramMap.containsKey("times")) {
            times = Integer.valueOf(paramMap.get("times").toString());
        }
        String redisKey = "redisQps";
        RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory());
        int no = redisAtomicInteger.getAndIncrement();
        //設(shè)置時(shí)間固定時(shí)間窗口長度 1S
        if (no == 0) {
            redisAtomicInteger.expire(1, TimeUnit.SECONDS);
        }
        //判斷是否超限  time=2 表示qps=3
        if (no > times) {
            throw new RuntimeException("qps refuse request");
        }
        //返回成功告知
        Map<string, object=""> map = new HashMap<>();
        map.put("success", "success");
        return map;
    }

    結(jié)果測試

    怎么用redis實(shí)現(xiàn)常見限流策略

    我們?cè)O(shè)置的qps=3 , 我們可以看到五個(gè)并發(fā)進(jìn)來后前三個(gè)正常訪問,后面兩個(gè)就失敗了。稍等一段時(shí)間我們?cè)诓l(fā)訪問,前三個(gè)又可以正常訪問。說明到了下一個(gè)時(shí)間窗口

    怎么用redis實(shí)現(xiàn)常見限流策略

    怎么用redis實(shí)現(xiàn)常見限流策略

    滑動(dòng)時(shí)間窗口算法

    • 針對(duì)固定時(shí)間窗口的缺點(diǎn)--臨界值出現(xiàn)雙倍流量問題。 我們的滑動(dòng)時(shí)間窗口就產(chǎn)生了。

    • 其實(shí)很好理解,就是針對(duì)固定時(shí)間窗口,將時(shí)間窗口統(tǒng)計(jì)從原來的固定間隔變成更加細(xì)度化的單元了。

    • 在上面我們固定時(shí)間窗口演示中我們?cè)O(shè)置的時(shí)間單元是1S 。 針對(duì)1S我們將1S拆成時(shí)間戳。

    • 固定時(shí)間窗口是統(tǒng)計(jì)單元隨著時(shí)間的推移不斷向后進(jìn)行。而滑動(dòng)時(shí)間窗口是我們認(rèn)為的想象出一個(gè)時(shí)間單元按照相對(duì)論的思想將時(shí)間固定,我們的抽象時(shí)間單元自己移動(dòng)。抽象的時(shí)間單元比實(shí)際的時(shí)間單元更小。

    • 讀者可以看下下面的動(dòng)圖,就可以理解了。

    怎么用redis實(shí)現(xiàn)常見限流策略

    優(yōu)點(diǎn)

    • 實(shí)質(zhì)上就是固定時(shí)間窗口算法的改進(jìn)。所以固定時(shí)間窗口的缺點(diǎn)就是他的優(yōu)點(diǎn)。

    • 內(nèi)部抽象一個(gè)滑動(dòng)的時(shí)間窗,將時(shí)間更加小化。存在邊界的問題更加小??蛻舾兄趿?。

    缺點(diǎn)

    • 不管是固定時(shí)間窗口算法還是滑動(dòng)時(shí)間窗口算法,他們都是基于計(jì)數(shù)器算法進(jìn)行優(yōu)化,但是他們對(duì)待限流的策略太粗暴了。

    • 為什么說粗暴呢,未限流他們正常放行。一旦達(dá)到限流后就會(huì)直接拒絕。這樣我們會(huì)損失一部分請(qǐng)求。這對(duì)于一個(gè)產(chǎn)品來說不太友好

    實(shí)現(xiàn)

    • 滑動(dòng)時(shí)間窗口是將時(shí)間更加細(xì)化,上面我們是通過redis#setnx實(shí)現(xiàn)的。這里我們就無法通過他統(tǒng)一記錄了。我們應(yīng)該加上更小的時(shí)間單元存儲(chǔ)到一個(gè)集合匯總。然后根據(jù)集合的總量計(jì)算限流。redis的zsett數(shù)據(jù)結(jié)構(gòu)就和符合我們的需求。

    • 為什么選擇zset呢,因?yàn)閞edis的zset中除了值以外還有一個(gè)權(quán)重。會(huì)根據(jù)這個(gè)權(quán)重進(jìn)行排序。如果我們將我們的時(shí)間單元及時(shí)間戳作為我們的權(quán)重,那么我們獲取統(tǒng)計(jì)的時(shí)候只需要按照一個(gè)時(shí)間戳范圍就可以了。

    • 因?yàn)閦set內(nèi)元素是唯一的,所以我們的值采用uuid或者雪花算法一類的id生成器

    controller

    @RequestMapping(value = "/startList",method = RequestMethod.GET)
        public Map<string,object> startList(@RequestParam Map<string, object=""> paramMap) {
            return testService.startList(paramMap);
        }

    service

    @RequestMapping(value = "/startList",method = RequestMethod.GET)
        public Map<string,object> startList(@RequestParam Map<string, object=""> paramMap) {
            return testService.startList(paramMap);
        }

    結(jié)果測試

    怎么用redis實(shí)現(xiàn)常見限流策略

    • 和固定時(shí)間窗口采用相同的并發(fā)。為什么上面也會(huì)出現(xiàn)臨界狀況呢。因?yàn)樵诖a里時(shí)間單元間隔比固定時(shí)間間隔采用還要大 。 上面演示固定時(shí)間窗口時(shí)間單元是1S出現(xiàn)了最壞情況。而滑動(dòng)時(shí)間窗口設(shè)計(jì)上就應(yīng)該間隔更短。而我設(shè)置成10S 也沒有出現(xiàn)壞的情況

    • 這里就說明滑動(dòng)比固定的優(yōu)處了。如果我們調(diào)更小應(yīng)該更加不會(huì)出現(xiàn)臨界問題,不過說到底他還是避免不了臨界出現(xiàn)的問題

    漏桶算法

    • 滑動(dòng)時(shí)間窗口雖然可以極大程度的規(guī)避臨界值問題,但是始終還是避免不了

    • 另外時(shí)間算法還有個(gè)致命的問題,他無法面對(duì)突如其來的大量流量,因?yàn)樗谶_(dá)到限流后直接就拒絕了其他額外流量

    • 針對(duì)這個(gè)問題我們繼續(xù)優(yōu)化我們的限流算法。 漏桶算法應(yīng)運(yùn)而生

    怎么用redis實(shí)現(xiàn)常見限流策略

    優(yōu)點(diǎn)

    • 面對(duì)限流更加的柔性,不在粗暴的拒絕。

    • 增加了接口的接收性

    • 保證下流服務(wù)接收的穩(wěn)定性。均勻下發(fā)

    缺點(diǎn)

    • 我覺得沒有缺點(diǎn)。非要雞蛋里挑骨頭那我只能說漏桶容量是個(gè)短板

    實(shí)現(xiàn)

    controller

    @RequestMapping(value = "/startLoutong",method = RequestMethod.GET)
    public Map<string,object> startLoutong(@RequestParam Map<string, object=""> paramMap) {
        return testService.startLoutong(paramMap);
    }

    service

    在service中我們通過redis的list的功能模擬出桶的效果。這里代碼是實(shí)驗(yàn)室性質(zhì)的。在真實(shí)使用中我們還需要考慮并發(fā)的問題

    @Override
    public Map<string, object=""> startLoutong(Map<string, object=""> paramMap) {
        String redisKey = "qpsList";
        Integer times = 100;
        if (paramMap.containsKey("times")) {
            times = Integer.valueOf(paramMap.get("times").toString());
        }
        Long size = redisTemplate.opsForList().size(redisKey);
        if (size >= times) {
            throw new RuntimeException("qps refuse request");
        }
        Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap);
        if (aLong > times) {
            //為了防止并發(fā)場景。這里添加完成之后也要驗(yàn)證。  即使這樣本段代碼在高并發(fā)也有問題。此處演示作用
            redisTemplate.opsForList().trim(redisKey, 0, times-1);
            throw new RuntimeException("qps refuse request");
        }
        Map<string, object=""> map = new HashMap<>();
        map.put("success", "success");
        return map;
    }

    下游消費(fèi)

    @Component
    public class SchedulerTask {
    
        @Autowired
        RedisTemplate redisTemplate;
    
        private String redisKey="qpsList";
    
        @Scheduled(cron="*/1 * * * * ?")
        private void process(){
            //一次性消費(fèi)兩個(gè)
            System.out.println("正在消費(fèi)。。。。。。");
            redisTemplate.opsForList().trim(redisKey, 2, -1);
        }
    
    }

    測試

    • 我們還是通過50并發(fā)循環(huán)10次訪問。我們可以發(fā)現(xiàn)只有在一開始能達(dá)到比較高的吞吐量。在隨后桶的容量滿了之后。而下游水滴速率比上游請(qǐng)求速率慢的情況下。只能以下游恒定的速度接收訪問。

    • 他的問題也暴露的很明顯。針對(duì)時(shí)間窗口的不足漏桶進(jìn)行的不足,但是仍是不足。無法徹底避免請(qǐng)求溢出的問題。

    • 請(qǐng)求溢出本身就是一種災(zāi)難性的問題。所有的算法目前都沒有解決這個(gè)問題。只是在減緩他帶來的問題

    怎么用redis實(shí)現(xiàn)常見限流策略

    令牌桶算法

    令牌桶和漏桶法是一樣的。只不過將桶的作用方向改變了一下。

    漏桶的出水速度是恒定的,如果流量突然增加的話我們就只能拒絕入池

    但是令牌桶是將令牌放入桶中,我們知道正常情況下令牌就是一串字符當(dāng)桶滿了就拒絕令牌的入池,但是面對(duì)高流量的時(shí)候正常加上我們的超時(shí)時(shí)間就留下足夠長的時(shí)間生產(chǎn)及消費(fèi)令牌了。這樣就盡可能的不會(huì)造成請(qǐng)求的拒絕

    最后,不論是對(duì)于令牌桶拿不到令牌被拒絕,還是漏桶的水滿了溢出,都是為了保證大部分流量的正常使用,而犧牲掉了少部分流量

    public Map<string, object=""> startLingpaitong(Map<string, object=""> paramMap) {
            String redisKey = "lingpaitong";
            String token = redisTemplate.opsForList().leftPop(redisKey).toString();
            //正常情況需要驗(yàn)證是否合法,防止篡改
            if (StringUtils.isEmpty(token)) {
                throw new RuntimeException("令牌桶拒絕");
            }
            Map<string, object=""> map = new HashMap<>();
            map.put("success", "success");
            return map;
        }
    @Scheduled(cron="*/1 * * * * ?")
        private void process(){
            //一次性生產(chǎn)兩個(gè)
            System.out.println("正在消費(fèi)。。。。。。");
            for (int i = 0; i < 2; i++) {
                redisTemplate.opsForList().rightPush(redisKey, i);
            }
        }

    到此,關(guān)于“怎么用redis實(shí)現(xiàn)常見限流策略”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI