基于令牌桶算法的Java限流實現(xiàn)

項目需要使用限流措施,查閱后主要使用令牌桶算法實現(xiàn)呈昔,為了更靈活的實現(xiàn)限流挥等,就自己實現(xiàn)了一個簡單的基于令牌桶算法的限流實現(xiàn)。

令牌桶算法描述

令牌桶這種控制機制基于令牌桶中是否存在令牌來指示什么時候可以發(fā)送流量堤尾。令牌桶中的每一個令牌都代表一個字節(jié)触菜。如果令牌桶中存在令牌,則允許發(fā)送流量哀峻;而如果令牌桶中不存在令牌涡相,則不允許發(fā)送流量。因此剩蟀,如果突發(fā)門限被合理地配置并且令牌桶中有足夠的令牌催蝗,那么流量就可以以峰值速率發(fā)送。

令牌桶算法圖描述

簡單的說就是育特,一邊請求時會消耗桶內(nèi)的令牌丙号,另一邊會以固定速率往桶內(nèi)放令牌。當消耗的請求大于放入的速率時缰冤,進行相應的措施犬缨,比如等待,或者拒絕等棉浸。

Java的簡單實現(xiàn)

為了更靈活的定制限流措施怀薛,自己實現(xiàn)了限流的部分代碼,如下:

/**
 * @author xiezhengchao
 * @since 18/1/3 上午9:45.
 * 限流器
 */
public class RateLimiter{

    private volatile int token;
    private final int originToken;

    private static Unsafe unsafe = null;
    private static final long valueOffset;
    private final Object lock = new Object();

    static {
        try {
            // 應用開發(fā)中使用unsafe對象必須通過反射獲取
            Class<?> clazz = Unsafe.class;
            Field f = clazz.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(clazz);
            valueOffset = unsafe.objectFieldOffset(RateLimiter.class.getDeclaredField("token"));
        } catch (Exception ex) {throw new Error(ex);}
    }

    public RateLimiter(int token){
        this.originToken = token;
        this.token = token;
    }

    /**
     * 獲取一個令牌
     */
    public boolean acquire(){
        int current = token;
        if(current<=0){
            // 保證在token已經(jīng)用光的情況下依然有獲取競爭的能力
            current = originToken;
        }

        long expect = 1000;// max wait 1s
        long future = System.currentTimeMillis()+expect;
        while(current>0){
            if(compareAndSet(current, current-1)){
                return true;
            }
            current = token;
            if(current<=0 && expect>0){
                // 在有效期內(nèi)等待通知
                synchronized (lock){
                    try {
                        lock.wait(expect);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                current = token;
                if(current<=0){
                    current = originToken;
                }
                expect = future - System.currentTimeMillis();
            }
        }
        return false;
    }

    private boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /**
     * 補充令牌
     */
    public void supplement(final ExecutorService executorService){
        this.token = originToken;
        executorService.execute(() -> {
            synchronized (lock){
                lock.notifyAll();
            }
        });
    }

}

核心代碼在acquire方法部分主要思路就是迷郑,優(yōu)先采用CAS進行自旋操作獲取令牌枝恋,一直嘗試令牌消耗光创倔,那么會進入等待,在定時線程調(diào)用supplement方法時焚碌,會喚醒所有等待線程畦攘,此時進入CAS進行嘗試消耗令牌,以此循環(huán)一直到設置的最大等待時間(代碼中的expect)消耗光十电,如果還沒獲得令牌知押,那么會返回false

這段代碼如果自己起一個線程進行限流,然后自己開個定時線程進行補充也可以鹃骂,但是實際運用中往往需要一個限流管理器來分配限流器朗徊,然后通過限流管理器統(tǒng)一的進行定時觸發(fā),這樣可以不用開很多的定時線程偎漫,同時通過線程池也避免了在定時線程競爭鎖時引發(fā)的過長等待造成定時線程不準的情況爷恳。

下面貼出限流管理器部分代碼

/**
 * @author xiezhengchao
 * @since 18/1/3 上午9:43.
 * 限流管理器
 */
@Component
public class ConfineManager{

    // 定時線程
    private final ScheduledThreadPoolExecutor scheduledCheck = new ScheduledThreadPoolExecutor(2);
    // 執(zhí)行補充線程池
    private final ExecutorService executorService = new ThreadPoolExecutor(5, 200,
            60L, TimeUnit.SECONDS, new SynchronousQueue<>(),
            new NamedThreadFactory("supplement",true,false));

    // 限流器容器
    private Map<String,RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();

    @PostConstruct
    public void init(){
        scheduledCheck.scheduleAtFixedRate(new SupplementRateLimiter(), 1, 1, TimeUnit.SECONDS);
    }

    @PreDestroy
    public void destroy(){
        scheduledCheck.shutdown();
    }

    /**
     * 通過key獲取相應的限流器
     */
    public void acquire(String key,int tokenCount){
        RateLimiter rateLimiter = rateLimiterMap.get(key);
        // 雙檢鎖確保安全創(chuàng)建
        if(rateLimiter==null){
            synchronized (this){
                // init RateLimiter
                rateLimiter = rateLimiterMap.computeIfAbsent(key, k -> new RateLimiter(tokenCount));
            }
        }
        // 嘗試獲取令牌
        if(!rateLimiter.acquire()){
            // 獲取失敗,根據(jù)實際情況進行處理象踊,這里直接拋異常了
            Assert.throwBizException(ErrorCode.API_CONFINE_RATE_LIMITER);
        }
    }

    /**
     * 補充相應的令牌數(shù)
     */
    private class SupplementRateLimiter implements Runnable{
        @Override
        public void run(){
            rateLimiterMap.values().forEach(rateLimiter -> rateLimiter.supplement(executorService));
        }
    }

}

代碼中主要是創(chuàng)建了定時線程温亲,補充令牌線程池。

部分代碼不是開源的杯矩,需要調(diào)整下栈虚,不影響主流程,代碼使用了一些spring的注解史隆。

其中的不足

在集群的環(huán)境下魂务,沒有考慮分布式的情況,也就是如果一個應用部署的限流是1s產(chǎn)生10個令牌泌射,假設部署了5個應用粘姜,那么實際1s可以產(chǎn)生50個令牌。如果需要考慮這部分熔酷,那么在CAS操作可以替換為通過redis的setnx來進行獲取鎖操作然后更新redis存儲對應的令牌孤紧,補充則直接設置更新redis對應的令牌數(shù)即可,這樣效率肯定比現(xiàn)在基于CAS操作低拒秘。

總結(jié)

實際上實現(xiàn)這個限流器更多的考慮是可以自行定義等待的最大時間号显,超時措施,定時補充令牌時間間隔等躺酒,同時也溫習了一下之前的并發(fā)知識押蚤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市羹应,隨后出現(xiàn)的幾起案子揽碘,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钾菊,死亡現(xiàn)場離奇詭異,居然都是意外死亡偎肃,警方通過查閱死者的電腦和手機煞烫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來累颂,“玉大人滞详,你說我怎么就攤上這事∥闪螅” “怎么了料饥?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長朱监。 經(jīng)常有香客問我岸啡,道長,這世上最難降的妖魔是什么赫编? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任巡蘸,我火速辦了婚禮,結(jié)果婚禮上擂送,老公的妹妹穿的比我還像新娘悦荒。我一直安慰自己,他們只是感情好嘹吨,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布搬味。 她就那樣靜靜地躺著,像睡著了一般蟀拷。 火紅的嫁衣襯著肌膚如雪碰纬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天问芬,我揣著相機與錄音嘀趟,去河邊找鬼。 笑死愈诚,一個胖子當著我的面吹牛她按,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炕柔,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼酌泰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匕累?” 一聲冷哼從身側(cè)響起陵刹,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎欢嘿,沒想到半個月后衰琐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體也糊,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年羡宙,在試婚紗的時候發(fā)現(xiàn)自己被綠了狸剃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡狗热,死狀恐怖钞馁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匿刮,我是刑警寧澤僧凰,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站熟丸,受9級特大地震影響训措,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜光羞,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一隙弛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狞山,春花似錦全闷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勘纯,卻和暖如春局服,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驳遵。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工淫奔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人堤结。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓唆迁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親竞穷。 傳聞我的和親對象是個殘疾皇子唐责,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348