一、限流的简介
在进行服务高可用设计时,为了避免突发的流量激增导致服务处理压力过大甚至造成的服务雪崩效应,往往会采用限流措施。从场景上看限流主要为了应对突发流量、恶意流量以及业务因素限制;限流策略上按端分类可以有请求端的限流、服务端的限流(侧重于入口)、微服务的限流(侧重于后端服务之间);限流从算法上分类主要有固定时间窗算法、滑动时间窗算法、漏桶算法、令牌桶算法等。
二、限流算法的不同方式
固定时间窗算法 : 基于一个确定起始时间(如按每分钟或每秒),用累计计数的方式统计时间段内的请求数量 ,在未达到上限值时允许请求通过让服务处理,在超出设定的上限值时拒绝请求,方式较为简单。
滑动时间窗算法: 由于固定时间窗算法在应对流量恰好落在前一区间尾端和后一区间前端这种场景时不能达到限流目的,所以优化衍生出用滑动窗口方式来动态维护时间窗口以达成限流。
漏桶算法:处于严格控制系统处理请求的频率,产生了漏桶方式的限流,漏桶中有水代表服务有能力处理请求,漏桶中滴漏出来的水滴代表了以相对稳定的间隔处理请求,在实际应用中也可以是将请求想象成是水滴,来了后就放入水桶(如果水桶满了则代表请求挤压达到上限,此时则做抛弃策略,防止过载),服务从漏洞中较为匀速的拿取水滴(请求)进行消费处理,达到较为良好的控制服务压力。
令牌桶算法:令牌桶相对于漏桶模式的区别在于将判断逻辑优先交给响应请求的表层,表层在响应请求时 需要优先从令牌桶中拿到令牌,能拿到则请求会被处理,拿不到则立刻做抛弃策略,快速返回,在流量高于限定时就会表现为请求部分失败,如果流量再增高,则整体失败率会逐步增高;同时由于令牌桶预先缓冲了一定数量的令牌在桶中,所以可以应对一些小规模突发流量。
三、代码的简略实现
固定时间窗算法
/**
* @author : wangchaodee
* @Description: 固定窗口限流方式
* <p>
* 参考 :https://time.geekbang.org/column/article/243961
*/
public class FixWindowRate implements RateLimitStrategy {
private Stopwatch stopwatch;
private AtomicInteger currentCount = new AtomicInteger(0);
private Lock lock = new ReentrantLock();
public FixWindowRate() {
this(Stopwatch.createStarted());
}
protected FixWindowRate(Stopwatch stopwatch) {
this.stopwatch = stopwatch;
}
@Override
public boolean canHandle(Request request) {
int cur = currentCount.incrementAndGet();
return cur <= LIMIT;
}
public TimerTask mockInnerTask(){
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
try {
if (lock.tryLock(TRY_LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
// 间隔着才会重置
if (stopwatch.elapsed(TimeUnit.MILLISECONDS) > TimeUnit.SECONDS.toMillis(DURATION)) {
currentCount.set(0);
stopwatch.reset();
stopwatch.start();
}
} finally {
lock.unlock();
}
} else {
log.info(" canHandle() can not get lock by lock timeout %s ms ", TRY_LOCK_TIMEOUT);
throw new RateLimitException("lock not get,timeout");
}
} catch (InterruptedException e) {
log.error(" canHandle() is interrupted bu lock timeout ");
throw new RateLimitException("lock not get ,interrupted");
}
}
};
return timerTask;
}
}
滑动时间窗算法
/**
* @author : wangchaodee
* @Description: 滑动窗口限流算法方式
*/
@Slf4j
public class SlidingWindowRate implements RateLimitStrategy{
Deque<Long> deque ;
Stopwatch stopwatch ;
private Lock lock = new ReentrantLock();
public SlidingWindowRate() {
this(new ArrayDeque<Long>(LIMIT) ,Stopwatch.createStarted());
}
public SlidingWindowRate(Deque deque ,Stopwatch stopwatch) {
this.deque = deque;
this.stopwatch = stopwatch;
}
/**
* 单点 每次都锁 消耗比较大
* @param request
* @return
*/
@Override
public boolean canHandle(Request request) {
long now = stopwatch.elapsed(TimeUnit.MILLISECONDS);
try {
if (lock.tryLock(TRY_LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
if(deque.size()< LIMIT){
deque.addLast(now);
return true ;
}else {
long interval = now - deque.getFirst();
// 大于限定时间 则表明限定时间内的 请求量小于阈值
if(interval >= TimeUnit.SECONDS.toMillis(DURATION)){
deque.removeFirst();
deque.addLast(now);
return true ;
}
}
} finally {
lock.unlock();
}
} else {
log.info(" canHandle() can not get lock by lock timeout %s ms ", TRY_LOCK_TIMEOUT);
throw new RateLimitException("lock not get,timeout");
}
} catch (InterruptedException e) {
log.error(" canHandle() is interrupted bu lock timeout ");
throw new RateLimitException("lock not get");
}
return false;
}
@Override
public TimerTask mockInnerTask() {
// 暂时不需要
return null;
}
}
漏桶算法
/**
* @author : wangchaodee
* @Description: 漏桶限流算法方式
*/
@Slf4j
public class LeakyBucketRate implements RateLimitStrategy{
private Stopwatch stopwatch;
private AtomicInteger currentCount = new AtomicInteger(0);
private Lock lock = new ReentrantLock();
public LeakyBucketRate() {
this(Stopwatch.createStarted());
}
protected LeakyBucketRate(Stopwatch stopwatch) {
this.stopwatch = stopwatch;
}
@Override
public boolean canHandle(Request request) {
int cur = currentCount.incrementAndGet();
return cur <= LIMIT ;
}
public TimerTask mockInnerTask(){
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
try {
if (lock.tryLock(TRY_LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
if (stopwatch.elapsed(TimeUnit.MILLISECONDS) > TimeUnit.SECONDS.toMillis(1)) {
int cur = currentCount.get() ;
int rate = LIMIT/DURATION ; // 消费速率
if(cur>=LIMIT){
currentCount.set( LIMIT- rate);
}else {
// 桶中消费 降为0 时不可消费
currentCount.set(Math.max(cur- rate , 0));
}
stopwatch.reset();
stopwatch.start();
}
} finally {
lock.unlock();
}
} else {
log.info(" canHandle() can not get lock by lock timeout %s ms ", TRY_LOCK_TIMEOUT);
throw new RateLimitException("lock not get,timeout");
}
} catch (InterruptedException e) {
log.error(" canHandle() is interrupted bu lock timeout ");
throw new RateLimitException("lock not get ,interrupted");
}
}
};
return timerTask;
}
}
令牌桶算法
/**
* @author : wangchaodee
* @Description: 令牌桶限流算法 模拟
*/
@Slf4j
public class TokenBucketRate implements RateLimitStrategy{
private Stopwatch stopwatch;
private AtomicLong lastHandled;
private AtomicInteger tokenAmount = new AtomicInteger(0);
private Lock lock = new ReentrantLock();
public TokenBucketRate() {
this(Stopwatch.createStarted());
}
protected TokenBucketRate(Stopwatch stopwatch) {
this.stopwatch = stopwatch;
lastHandled = new AtomicLong(stopwatch.elapsed(TimeUnit.MILLISECONDS));
}
@Override
public boolean canHandle(Request request) {
int cur = tokenAmount.decrementAndGet();
return cur>=0;
}
public TimerTask mockInnerTask(){
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
try {
if (lock.tryLock(TRY_LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
if (stopwatch.elapsed(TimeUnit.MILLISECONDS) > TimeUnit.SECONDS.toMillis(1)) {
int cur = tokenAmount.get();
int rate = LIMIT/DURATION;
if(cur<=0){
// 令牌桶空了时 直接设置令牌
tokenAmount.set(rate);
}else {
// // 令牌桶非空时 累加令牌 但不能超上限
tokenAmount.set(Math.min(LIMIT,cur + rate));
}
stopwatch.reset();
stopwatch.start();
}
} finally {
lock.unlock();
}
} else {
log.info(" canHandle() can not get lock by lock timeout %s ms ", TRY_LOCK_TIMEOUT);
throw new RateLimitException("lock not get,timeout");
}
} catch (InterruptedException e) {
log.error(" canHandle() is interrupted bu lock timeout ");
throw new RateLimitException("lock not get ,interrupted");
}
}
};
return timerTask;
}
}
模拟测试
private Map<Integer ,Integer> execute(RateLimitHandler rateLimitHandler , List<Request> requestList){
ExecutorService executor= Executors.newFixedThreadPool(5);
List<Callable<Map<Integer ,Integer>>> tasks = generateTasks(rateLimitHandler,requestList);
for (int i = 0; i <threadNum ; i++) {
executor.submit(tasks.get(i));
}
Map<Integer ,Integer> statusCount = new HashMap<>() ;
try {
for (int i = 0; i < threadNum; i++) {
Map<Integer, Integer> taskRes = tasks.get(i).call();
for (Map.Entry<Integer, Integer> res : taskRes.entrySet()) {
statusCount.put(res.getKey(), statusCount.getOrDefault(res.getKey(),0) + res.getValue());
}
}
}catch (Exception e){
System.out.println("get result error ");
}
return statusCount ;
}
@Test
public void testFixRateLimit() {
RateLimitHandler rateLimitHandler = new RateLimitHandler(new FixWindowRate());
System.out.println("rate limit Strategy : " + rateLimitHandler.getStrategy().getClass().getSimpleName()) ;
Stopwatch stopwatch = Stopwatch.createStarted() ;
rateLimitHandler.schedule();
Map<Integer ,Integer> statusCount = execute(rateLimitHandler,generateBlankRequestList());
System.out.println("time : " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
for (Map.Entry<Integer,Integer> res : statusCount.entrySet()) {
System.out.printf("key %d , nums : %d \n ", res.getKey() ,res.getValue());
}
}