滑動(dòng)計(jì)數(shù)器實(shí)現(xiàn)算法對(duì)比

一:目標(biāo):實(shí)現(xiàn)任意滑動(dòng)計(jì)數(shù)器的抽象,保證高性能

此抽象可以提供給限流 熔斷 防刷等統(tǒng)計(jì)模塊使用,進(jìn)行任意時(shí)間的統(tǒng)計(jì)與計(jì)數(shù)氧苍,高性能,線程安全是基本要求

二:算法如下

  1. 數(shù)組模擬時(shí)間輪
  2. 當(dāng)前使用的數(shù)據(jù)為當(dāng)前時(shí)間對(duì)數(shù)組大小進(jìn)行取模確定.
    累加邏輯為:當(dāng)前數(shù)組元素往前n個(gè)大小進(jìn)行累加
  3. 清理邏輯分兩種實(shí)現(xiàn)
    3.1 數(shù)組大小=滑動(dòng)窗口大小+未來(lái)n個(gè)時(shí)間單位的大小,定時(shí)任務(wù)提前清理未來(lái)n個(gè)時(shí)間單位大小
    3.2 使用時(shí)清理,不使用buffer.需要一個(gè)紀(jì)錄最后一次使用位置的指針,當(dāng)使用時(shí)清理最后一次位置到當(dāng)前位置的數(shù)據(jù)包括當(dāng)前位置,考慮并發(fā)調(diào)用的 情況,此清理過(guò)程只能一個(gè)線程進(jìn)行清理,
    清理后把最后一次的指針指向當(dāng)前位置,釋放鎖,其他線程才能進(jìn)入再次執(zhí)行次邏輯,通常其他線程進(jìn)行后發(fā)現(xiàn)當(dāng)前指針跟自己持有的指針相同, 不清理跳過(guò)备图。統(tǒng)計(jì)邏輯也類似
    3.3 3.1邏輯較為簡(jiǎn)單,3.2比較復(fù)雜,但是少了提前預(yù)留的n個(gè)大小
    可以用次抽象實(shí)現(xiàn)任意單位時(shí)間的n秒滑動(dòng)計(jì)數(shù)器

三:實(shí)現(xiàn)

提前清理

public class TimeRollingScheduleCleanCounter {


  private static final ScheduledExecutorService service = Executors.newScheduledThreadPool(1);


  private static final ConcurrentLinkedQueue<TimeRollingScheduleCleanCounter> queue = new ConcurrentLinkedQueue<>();

  private static final int nextBufferSize=10;


  public static void register(TimeRollingScheduleCleanCounter timeRollingCounterScheduleClean) {
      queue.add(timeRollingCounterScheduleClean);
  }

  static {
      service.scheduleAtFixedRate(new Runnable() {
          @Override
          public void run() {

              for (TimeRollingScheduleCleanCounter clean : queue) {
                  int index = currentIndex(clean.getBucketsSize());
                  for(int i=0;i<nextBufferSize;i++){
                      clean.buckets[(index+i+clean.getBucketsSize())%clean.getBucketsSize()].set(0);
                  }
              }
          }
      }, 1, 1, TimeUnit.SECONDS);
  }


  /**
   * 滑動(dòng)計(jì)數(shù)器對(duì)應(yīng)的bucket
   */
  private AtomicLong[] buckets;

  private int bucketsSize;

  public TimeRollingScheduleCleanCounter(int size) {
      bucketsSize=size+nextBufferSize;
      buckets = new AtomicLong[bucketsSize];
      for (int i = 0; i < buckets.length; i++) {
          buckets[i] = new AtomicLong(0);
      }
      register(this);
  }

  public void add(Long count) {
      buckets[currentIndex(bucketsSize)].getAndAdd(count);
  }


  public long accumulative(int n) {
      if (n > bucketsSize-nextBufferSize) {
          throw new IllegalArgumentException("params is too large");
      }
      int start = currentIndex(bucketsSize);
      long total = 0;
      for (int i = 0; i < n; i++) {
          total += buckets[(start - i+bucketsSize) % bucketsSize].get();
      }
      return total;
  }

  public int getBucketsSize() {
      return bucketsSize;
  }

  private static int currentIndex(int bucketsSize) {
      long currentTime = System.currentTimeMillis() / 1000;
      return (int) (currentTime % bucketsSize);
  }

  public long increment() {
      return buckets[currentIndex(bucketsSize)].incrementAndGet();
  }
  }

使用時(shí)清理

public class TimeRollingCounter {

  private static Logger logger = LoggerFactory.getLogger(TimeRollingCounter.class);
   
 /**
  * 滑動(dòng)計(jì)數(shù)器對(duì)應(yīng)的bucket
  */
 private AtomicInteger[] buckets;

 //private LongAdder[] longAdders;

 /**
  * bucket對(duì)應(yīng)的時(shí)間
  */
 private AtomicLong currentBucketTime;

 /**
  * 桶的默認(rèn)大小
  */
 private  int bucketSize =1;

 private AtomicBoolean windowRolling = new AtomicBoolean(false);

 private static final long MILLIS_IN_ONE_SECOND=1000L;//1秒等于1000毫秒
  
 /**
  * 給當(dāng)前時(shí)間的計(jì)數(shù)器增加1
  * 如果當(dāng)前時(shí)間和桶的當(dāng)前時(shí)間差大于BUCKET_SIZE,重置所有的bucket
  * @return
  */
 public int increment(){
    final long targetBucketTime = getNowSecond();//秒轉(zhuǎn)換成毫秒
    final int targetBucketIndex = this.calIndex(targetBucketTime);
      //窗口滑動(dòng),重置計(jì)數(shù)器
      this.windowRolling(targetBucketTime,this.currentBucketTime.get());
    //增加當(dāng)前計(jì)數(shù)
    buckets[targetBucketIndex].incrementAndGet();
    //返回當(dāng)前計(jì)數(shù)值
    return buckets[targetBucketIndex].intValue();
 }

 /**
  * 獲取之前N秒的計(jì)數(shù)之和
  * @param n
  * @return
  */
 public int accumulative(int n){
    if(n> bucketSize){
       n= bucketSize;
    }
    final long targetBucketTime = getNowSecond();
    final int targetBucketIndex = this.calIndex(targetBucketTime);
      //窗口滑動(dòng),重置計(jì)數(shù)器
      this.windowRolling(targetBucketTime,this.currentBucketTime.get());
    AtomicInteger beforeSum = new AtomicInteger(0);
    for(int i=0;i<n;i++){
       beforeSum.addAndGet(buckets[(targetBucketIndex+ bucketSize -i)% bucketSize].intValue());
    }
    return beforeSum.intValue();
 }


 /**
  * 初始化一個(gè)當(dāng)前時(shí)間的全0計(jì)數(shù)器,并可以設(shè)置bucket大小
  */
 public TimeRollingCounter(int size, TimeUnit timeUnit){
    final long currentTime = getNowSecond();
    bucketSize =size;
    buckets=new AtomicInteger[bucketSize];
    for(int i = 0; i< bucketSize; i++){
       buckets[i]=new AtomicInteger(0);
    }
    currentBucketTime=new AtomicLong(currentTime);
 }

 /**
  * 重置計(jì)數(shù)器
  */
 public void reset(){
    final long currentTime = getNowSecond();;
    for(AtomicInteger counter:buckets){
       counter.set(0);
    }
    currentBucketTime.set(currentTime);
 }

  /**
   * 窗口滑動(dòng),使用自旋滑動(dòng)
   * @param targetBucketTime
   * @param currentBucketTime
   */
  private void windowRolling(final long targetBucketTime,Long currentBucketTime){
      if(targetBucketTime > currentBucketTime){

          while(!windowRolling.compareAndSet(false,true)){
       }
       try {
          //重新獲取值
          currentBucketTime = this.currentBucketTime.get();
          //計(jì)數(shù)器已經(jīng)被其它線程滑動(dòng)了,不再滑動(dòng),防止誤操作數(shù)據(jù)
          if (currentBucketTime >= targetBucketTime) {
             return ;
          }
          final int sep = this.calSep(currentBucketTime, targetBucketTime);
          final int currentIndex = this.calIndex(currentBucketTime);
          //重置計(jì)數(shù)器
          this.resetBucket(currentIndex, sep);
          //重新設(shè)置當(dāng)前時(shí)間
          this.currentBucketTime.set(targetBucketTime);

       } catch(Exception e){
          logger.error("Window rolling error",e);
       }finally {
          windowRolling.set(false);
       }
      }
  }

 /**
  * 時(shí)間差大于BUCKET_SIZE,重置bucket
  * 否則重置兩個(gè)窗口區(qū)間的臟數(shù)據(jù),注意這是一個(gè)循環(huán)數(shù)組
  * @param currentBucketIndex
  * @param sep
  */
 private void resetBucket(final int currentBucketIndex,int sep){
    //時(shí)間差大于buck_size,重置所有的bucket
    if(sep>= bucketSize){
       for(AtomicInteger counter:buckets){
          counter.set(0);
       }
    }else {
       //循環(huán)隊(duì)列
       if(sep<0){
          sep= bucketSize +sep;
       }
       //時(shí)間差在1到sep+1秒之間,重置之間的區(qū)間,包括新使用的本身
       for(int i=1;i<sep+1;i++){
          buckets[(currentBucketIndex+i)% bucketSize].set(0);
       }
    }
     
 }

 /**
  * 計(jì)算指定時(shí)間對(duì)應(yīng)的bucket索引
  * @param currentTime
  * @return
  */
 private int calIndex(Long currentTime){
    int currIndex = Long.valueOf(currentTime% bucketSize).intValue();
    return currIndex;
 }

 /**
  * 計(jì)算兩個(gè)指定時(shí)間差,以秒為單位,可能為負(fù)數(shù)
  * @param beforeTime
  * @param currentTime
  * @return
  */
 private int calSep(Long beforeTime,Long currentTime){
    return Long.valueOf(currentTime-beforeTime).intValue();
 }


 public int getBucketSize() {
    return bucketSize;
 }

 /**
  * 獲取當(dāng)前的秒數(shù)
  * @return
  */
 public static long getNowSecond(){
    return System.currentTimeMillis()/MILLIS_IN_ONE_SECOND;
 }
  }

三:選擇哪種

1.理解成本

代碼更多的時(shí)間是在做維護(hù)震桶,上面復(fù)雜的寫法很多時(shí)候在做解釋,review更是耗費(fèi)巨大時(shí)間再沧,修改一行代碼也設(shè)計(jì)成本很高尼夺,以前的實(shí)現(xiàn)也有bug,參考我之前的分析

[janus本地防刷代碼問(wèn)題分析]

2.gc成本

創(chuàng)建是注冊(cè)到隊(duì)列上炒瘸,無(wú)法回收掉淤堵,因?yàn)闊o(wú)法判斷何時(shí)移除。當(dāng)然可以換一種實(shí)現(xiàn)顷扩,提供一個(gè)可以拿到當(dāng)前使用所有計(jì)數(shù)器的接口拐邪。

我們目前場(chǎng)景用完后都會(huì)放緩存隊(duì)列,暫時(shí)不存在該問(wèn)題

3.清理?yè)p耗

定時(shí)器一秒清理未來(lái)buffer 數(shù)量隘截,但我們場(chǎng)景存在百萬(wàn)級(jí)別的數(shù)量扎阶,是否能快速清理掉,需要測(cè)試1s的清理速度婶芭,理論上兩次清理完成間隔相差十秒就滿足條件

根據(jù)我們場(chǎng)景东臀,大概會(huì)有將近百萬(wàn)的計(jì)數(shù)器(統(tǒng)計(jì)ip session的防刷)本地測(cè)試能在1s內(nèi)清理掉,但cpu卻一致高負(fù)載犀农,此方案在百萬(wàn)級(jí)別的情況下會(huì)損耗不少cpu資源

數(shù)量增大到5百萬(wàn)惰赋,有其他使用cpu時(shí),清理時(shí)間不穩(wěn)定呵哨,但在預(yù)期內(nèi)可以清理掉赁濒。

測(cè)試過(guò)程遇到卡住,為old區(qū)滿掉孟害,頻繁gc但無(wú)空間所致拒炎。

綜上,3的cpu消耗不能容忍挨务,只能選擇較為復(fù)雜的方案實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末击你,一起剝皮案震驚了整個(gè)濱河市玉组,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丁侄,老刑警劉巖球切,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異绒障,居然都是意外死亡吨凑,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門户辱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸵钝,“玉大人,你說(shuō)我怎么就攤上這事庐镐《魃蹋” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵必逆,是天一觀的道長(zhǎng)怠堪。 經(jīng)常有香客問(wèn)我,道長(zhǎng)名眉,這世上最難降的妖魔是什么粟矿? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮损拢,結(jié)果婚禮上陌粹,老公的妹妹穿的比我還像新娘。我一直安慰自己福压,他們只是感情好掏秩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荆姆,像睡著了一般蒙幻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胆筒,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天邮破,我揣著相機(jī)與錄音,去河邊找鬼腐泻。 笑死决乎,一個(gè)胖子當(dāng)著我的面吹牛队询,可吹牛的內(nèi)容都是我干的派桩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蚌斩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼铆惑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤员魏,失蹤者是張志新(化名)和其女友劉穎丑蛤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撕阎,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡受裹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虏束。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棉饶。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖镇匀,靈堂內(nèi)的尸體忽然破棺而出照藻,到底是詐尸還是另有隱情,我是刑警寧澤汗侵,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布幸缕,位于F島的核電站,受9級(jí)特大地震影響晰韵,放射性物質(zhì)發(fā)生泄漏发乔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一雪猪、第九天 我趴在偏房一處隱蔽的房頂上張望列疗。 院中可真熱鬧,春花似錦浪蹂、人聲如沸抵栈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)古劲。三九已至,卻和暖如春缰猴,著一層夾襖步出監(jiān)牢的瞬間产艾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工滑绒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闷堡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓疑故,卻偏偏與公主長(zhǎng)得像杠览,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纵势,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理踱阿,服務(wù)發(fā)現(xiàn)管钳,斷路器,智...
    卡卡羅2017閱讀 134,669評(píng)論 18 139
  • Java經(jīng)典問(wèn)題算法大全 /*【程序1】 題目:古典問(wèn)題:有一對(duì)兔子软舌,從出生后第3個(gè)月起每個(gè)月都生一對(duì)兔子才漆,小兔子...
    趙宇_阿特奇閱讀 1,869評(píng)論 0 2
  • 1.HashMap是一個(gè)數(shù)組+鏈表/紅黑樹的結(jié)構(gòu),數(shù)組的下標(biāo)在HashMap中稱為Bucket值佛点,每個(gè)數(shù)組項(xiàng)對(duì)應(yīng)的...
    誰(shuí)在烽煙彼岸閱讀 1,026評(píng)論 2 2
  • 印象深刻的三個(gè)部分:1.老師繼續(xù)講完了感覺與知覺部分的知識(shí)醇滥。2.和同學(xué)們進(jìn)行了深入淺出的科研交流。3.我又一次走上...
    名字被狗取閱讀 192評(píng)論 2 2
  • 作者是一位做過(guò)美國(guó)中央情報(bào)局的間諜超营,他覺得那是他一生中最激動(dòng)人心的日子腺办,用不同的身份生活和工作。后來(lái)他成為華盛頓間...
    雨霏Maggie媽閱讀 217評(píng)論 0 0