文章目录
- 一、限流算法
- 1. 漏桶算法
- 2. 令牌桶算法
- 二、令牌桶算法VS漏桶算法
- 三、解决方案
- 1. 使用Guava的RateLimiter进行限流控制(单机)
- 2. 使用Semphore进行并发流控(单机)
- 3. redisson实现分布式限流(集群)
工作中对外提供的API 接口设计都要考虑限流,如果不考虑限流,会成系统的连锁反应,轻者响应缓慢,重者系统宕机,整个业务线崩溃,如何应对这种情况呢,我们可以对请求进行引流或者直接拒绝等操作,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
缓存:缓存的目的是提升系统访问速度和增大系统处理容量
降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
一、限流算法
常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。
1. 漏桶算法
把请求比作是水,水来了都先放进桶里,并以限定的速度出水,当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务。
漏斗有一个进水口 和 一个出水口,出水口以一定速率出水,并且有一个最大出水速率:
在漏斗中没有水的时候,
- 如果进水速率小于等于最大出水速率,那么,出水速率等于进水速率,此时,不会积水
- 如果进水速率大于最大出水速率,那么,漏斗以最大速率出水,此时,多余的水会积在漏斗中
在漏斗中有水的时候
- 出水口以最大速率出水
- 如果漏斗未满,且有进水的话,那么这些水会积在漏斗中
- 如果漏斗已满,且有进水的话,那么这些水会溢出到漏斗之外
2. 令牌桶算法
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。
二、令牌桶算法VS漏桶算法
漏桶
漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
令牌桶
生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。
三、解决方案
1. 使用Guava的RateLimiter进行限流控制(单机)
Guava是google提供的java扩展类库,其中的限流工具类RateLimiter采用的就是令牌桶算法。RateLimiter 从概念上来讲,速率限制器会在可配置的速率下分配许可证,如果必要的话,每个acquire() 会阻塞当前线程直到许可证可用后获取该许可证,一旦获取到许可证,不需要再释放许可证。通俗的讲RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。
有一点很重要,那就是请求的许可数从来不会影响到请求本身的限制(调用acquire(1) 和调用acquire(1000) 将得到相同的限制效果,如果存在这样的调用的话),但会影响下一次请求的限制,也就是说,如果一个高开销的任务抵达一个空闲的RateLimiter,它会被马上许可,但是下一个请求会经历额外的限制,从而来偿付高开销任务。注意:RateLimiter 并不提供公平性的保证。
2. 使用Semphore进行并发流控(单机)
Java 并发库的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
3. redisson实现分布式限流(集群)
@Slf4j
public class TestRedissonRate {
private final RedissonClient redisClient;
private final String key = "msgRateLimiter:" + "test";
private final int limiter = 10000;
@Autowired
public TestRedissonRate(RedissonClient redisClient) {
this.redisClient = redisClient;
}
//服务启动的时候,先清一下 redis,防止 count 出错
public void reload() {
RMapCache<String, Integer> msgRateLimit =
redisClient.getMapCache(key, IntegerCodec.INSTANCE);
if (msgRateLimit.containsKey(key)) {
msgRateLimit.delete();
}
}
//该方法可以配合 mq,结果是 true 的话就 ack,false 的话就 reject
public boolean handleMessage() {
//分布式场景下的限流
//String key = "msgRateLimiter:" + MsgConstants.MsgType.APP_PUSH[0];
RMapCache<String, Integer> msgRateLimit =
redisClient.getMapCache(key, IntegerCodec.INSTANCE);
Integer count;
try {
msgRateLimit.putIfAbsent(key, 0, 1L, TimeUnit.SECONDS);
count = msgRateLimit.addAndGet(key, 1);
log.info("get redis counter:{}", count);
if (count < limiter) {
//TODO 此处是要执行的代码
return true;
}
log.warn("超过限流:{}", count);
} catch (Exception e) {
log.error("err", e);
}
return false;
}
}
**最后:**进行限流控制还可以有很多种方法,针对不同的场景各有优劣,例如通过LongAdder计数器控制、使用MQ消息队列进行流量消峰等等。