斷尾求生 —— 簡(jiǎn)單限流

限流算法在分布式領(lǐng)域是一個(gè)經(jīng)常被提起的話題,當(dāng)系統(tǒng)的處理能力有限時(shí),如何阻止計(jì)劃外的請(qǐng)求繼續(xù)對(duì)系統(tǒng)施壓,這是一個(gè)需要重視的問題。

除了控制流量,限流還有一個(gè)應(yīng)用目的是用于控制用戶行為,避免垃圾請(qǐng)求。比如在UGC 社區(qū),用戶的發(fā)帖、回復(fù)、點(diǎn)贊等行為都要嚴(yán)格受控,一般要嚴(yán)格限定某行為在規(guī)定時(shí)間內(nèi)允許的次數(shù),超過了次數(shù)那就是非法行為。對(duì)非法行為,業(yè)務(wù)必須規(guī)定適當(dāng)?shù)膽吞幉呗浴?/p>

如何使用 Redis 來實(shí)現(xiàn)簡(jiǎn)單限流策略?

首先我們來看一個(gè)常見 的簡(jiǎn)單的限流策略。系統(tǒng)要限定用戶的某個(gè)行為在指定的時(shí)間里只能允許發(fā)生 N 次,如何使用 Redis 的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)這個(gè)限流的功能?

這個(gè)限流需求中存在一個(gè)滑動(dòng)時(shí)間窗口,想想 zset 數(shù)據(jù)結(jié)構(gòu)的 score 值,是不是可以通過 score 來圈出這個(gè)時(shí)間窗口來。而且我們只需要保留這個(gè)時(shí)間窗口,窗口之外的數(shù)據(jù)都可以砍掉。那這個(gè) zset 的 value 填什么比較合適呢?它只需要保證唯一性即可,用 uuid 會(huì)比較浪費(fèi)空間,那就改用毫秒時(shí)間戳吧。

Redis簡(jiǎn)單限流策略

如圖所示,用一個(gè) zset 結(jié)構(gòu)記錄用戶的行為歷史,每一個(gè)行為都會(huì)作為 zset 中的一個(gè)key 保存下來。同一個(gè)用戶同一種行為用一個(gè) zset 記錄。

為節(jié)省內(nèi)存,我們只需要保留時(shí)間窗口內(nèi)的行為記錄,同時(shí)如果用戶是冷用戶,滑動(dòng)時(shí)間窗口內(nèi)的行為是空記錄,那么這個(gè) zset 就可以從內(nèi)存中移除,不再占用空間。

通過統(tǒng)計(jì)滑動(dòng)窗口內(nèi)的行為數(shù)量與閾值 max_count 進(jìn)行比較就可以得出當(dāng)前的行為是否允許。用代碼表示如下:

/**
 * @Auther: majx2
 * @Date: 2019-3-22 14:21
 * @Description:
 */
public class SimpleRateLimiter {

    private Jedis jedis = RedisDS.create().getJedis();

    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        pipe.zadd(key, nowTs, "" + nowTs);
        // 移除有序集中,指定score區(qū)間內(nèi)的所有成員
        pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
        Response<Long> count = pipe.zcard(key);
        pipe.expire(key, period + 1);
        pipe.exec();
        pipe.close();
        return count.get() <= maxCount;
    }
    public static void main(String[] args) throws InterruptedException {
        final SimpleRateLimiter limiter = new SimpleRateLimiter();
        for(int i=0;i<20;i++) {
            System.out.println(limiter.isActionAllowed("majx2", "reply", 60, 5));
        }
    }
}

整體思路就是:每一個(gè)行為到來時(shí),都維護(hù)一次時(shí)間窗口。將時(shí)間窗口外的記錄全部清理掉,只保留窗口內(nèi)的記錄。zset 集合中只有 score 值非常重要,value 值沒有特別的意義,只需要保證它是唯一的就可以了。

因?yàn)檫@幾個(gè)連續(xù)的 Redis 操作都是針對(duì)同一個(gè) key 的,使用 pipeline 可以顯著提升Redis 存取效率。但這種方案也有缺點(diǎn),因?yàn)樗涗洉r(shí)間窗口內(nèi)所有的行為記錄,如果這個(gè)量很大,比如限定 60s 內(nèi)操作不得超過 100w 次這樣的參數(shù),它是不適合做這樣的限流的,因?yàn)闀?huì)消耗大量的存儲(chǔ)空間。

下一節(jié)我們將引入高級(jí)限流算法——漏斗限流。

本文基于《Redis深度歷險(xiǎn):核心原理和應(yīng)用實(shí)踐》一文的JAVA實(shí)踐。更多文章請(qǐng)參考:高性能緩存中間件Redis應(yīng)用實(shí)戰(zhàn)(JAVA)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容