引贴: 高并发系统之限流特技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));
思路: 水(请求)先进入到漏桶里,漏桶以必定的速度出水,当水流入速度过大会直接溢出,能够看出漏桶算法能强行限制数据的传输速率。(相似于队列,控制出队速率)
对于不少应用场景来讲,除了要求可以限制数据的平均传输速率外,还要求容许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
系统会以一个恒定的速度往桶里放入令牌,而若是请求须要被处理,则须要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。