引貼: 高併發系統之限流特技java
在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流算法
緩存
緩存的目的是提高系統訪問速度和增大系統處理容量降級
降級是當服務出現問題或者影響到核心流程時,須要暫時屏蔽掉,待高峯或者問題解決後再打開限流
限流的目的是經過對併發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則能夠拒絕服務、排隊或等待、降級等處理由 guava 提供,經常使用在削峯控流的場景中。
Java 併發庫 的Semaphore信號量控制也能夠作到必定的控制: 經過 acquire() 獲取一個許可,若是沒有就等待,而 release() 釋放一個許可 。
固然在具體使用的業務場景中,既然都要限流了,要考慮是否線程池的配置合理!spring
RateLimiter的兩種模式: SmoothBursty(穩定模式) & SmoothWarmingUp(漸進模式) 。
使用時須要考慮初始化的時機,避免剛初始化即有大量訪問併發形成等待(acquire)或訪問拒絕(tryAcquire)。(可在spring容器初始化階段提早構建)
也須要考慮SmoothBursty模式下的必定程度突發形成的峯值問題。緩存
package com.noob; import java.lang.reflect.Field; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.concurrent.TimeUnit; import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.RateLimiter; public class RateLimiterTest { public static void main(String args[]) throws Exception { RateLimiterWrapper limiter = new RateLimiterWrapper( RateLimiter.create(2)); limiter.acquire(); limiter.acquire(); limiter.acquire(); limiter.acquire(); System.out.println("Thread.sleep 2s"); try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } limiter.acquire(); limiter.acquire(); limiter.acquire(); limiter.acquire(); } static class RateLimiterWrapper { private RateLimiter limiter; private Stopwatch stopwatch; private String simpleName; private Field storedPermits; private Field nextFreeTicketMicros; public RateLimiterWrapper(RateLimiter limiter) throws Exception { this.limiter = limiter; Class<?> clas = limiter.getClass(); simpleName = clas.getSimpleName(); storedPermits = clas.getSuperclass().getDeclaredField( "storedPermits"); storedPermits.setAccessible(true); nextFreeTicketMicros = clas.getSuperclass().getDeclaredField( "nextFreeTicketMicros"); nextFreeTicketMicros.setAccessible(true); Field stopwatchField = clas.getSuperclass().getSuperclass() .getDeclaredField("stopwatch"); stopwatchField.setAccessible(true); Object sleepingStopwatch = stopwatchField.get(limiter); Field stopwatchFiled = (sleepingStopwatch.getClass() .getDeclaredField("stopwatch")); stopwatchFiled.setAccessible(true); stopwatch = (Stopwatch) stopwatchFiled.get(sleepingStopwatch); System.out .println(String .format("%s -> 初始化階段: init-storedPermits: %s, init-nextFreeTicketMicros: %s", simpleName, storedPermits.get(limiter), nextFreeTicketMicros.get(limiter))); } long readMicros() { return stopwatch.elapsed(TimeUnit.MICROSECONDS); } double acquire() throws Exception { long reqTimeMirco = readMicros(); Object beforeStoredPermits = storedPermits.get(this.limiter); Object beforeNextFreeTicketMicros = nextFreeTicketMicros .get(this.limiter); double waitMirco = this.limiter.acquire(); Object afterStoredPermits = storedPermits.get(this.limiter); Object afterNextFreeTicketMicros = nextFreeTicketMicros .get(this.limiter); System.out .println(String .format("reqTimeMirco: %s, before-storedPermits: %s, before-nextFreeTicketMicros: %s, waitSeconds: %ss, after-storedPermits: %s, after-nextFreeTicketMicros: %s", reqTimeMirco, convert(beforeStoredPermits), beforeNextFreeTicketMicros, convert(waitMirco), convert(afterStoredPermits), afterNextFreeTicketMicros)); return waitMirco; } } public static BigDecimal convert(Object o) { return new BigDecimal(String.valueOf(o)).setScale(4, RoundingMode.HALF_UP); } }
此處能夠看到設置的桶容量爲2(即容許的突發量),這是由於SmoothBursty中有一個參數:最大突發秒數(maxBurstSeconds)默認值是1s,突發量(桶容量)=速率*maxBurstSeconds,因此本示例 桶容量(突發量)爲2。安全
發現: 在線程等待後,是第四個請求才開始有等待!併發
因此按這個邏輯:
此時的stableIntervalMicros = 500000;app
第一次請求中: 高併發
雖然 before-nextFreeTicketMicros = 745,但在reserveEarliestAvailable方法中返回的 nextFreeTicketMicros 的值就是reqTimeMirco值19298 ,因此與請求時間比較得到的等待時間waitSeconds = 0s。工具
差額令牌時間 = (需求量 1 - (請求時間 19298 - 原令牌可供時間 745)/ 單個令牌生成時間 500000) * 單個令牌生成時間 500000 = 481447; 因此更新以後的nextFreeTicketMicros = 481447+ 19298 =500745 !ui
第二次請求:
reqTimeMirco < before-nextFreeTicketMicros,因此不計算可以生成令牌數量。 直接比較等待時間 waitSeconds = 500745 - 21164= 479581= 0.4796s;
更新以後的nextFreeTicketMicros = 500745 + 1 * 500000 = 1000745 !
由於SmoothBursty容許必定程度的突發,會擔憂若是容許這種突發,假設忽然間來了很大的流量,那麼系統極可能扛不住這種突發。所以須要一種平滑速率的限流工具,從而系統冷啓動後慢慢的趨於平均固定速率(即剛開始速率小一些,而後慢慢趨於設置的固定速率)
public static void main(String args[]) throws Exception { RateLimiterWrapper limiter = new RateLimiterWrapper(RateLimiter.create( 5, 1, TimeUnit.SECONDS)); limiter.acquire(); limiter.acquire(); limiter.acquire(); limiter.acquire(); limiter.acquire(); limiter.acquire(); limiter.acquire(); }
RateLimiterWrapper limiter = new RateLimiterWrapper(RateLimiter.create(5, 3, TimeUnit.SECONDS));
思路: 水(請求)先進入到漏桶裏,漏桶以必定的速度出水,當水流入速度過大會直接溢出,能夠看出漏桶算法能強行限制數據的傳輸速率。(相似於隊列,控制出隊速率)
對於不少應用場景來講,除了要求可以限制數據的平均傳輸速率外,還要求容許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。
系統會以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。