一、引言
最近有并发较高影响服务稳定性,之前博主分享过自己封装的计算限流工具,github地址:
GitHub - SongTing0711/count-limit
主要是针对计算资源如:CPU、内存等,使用的其实是加权计数器的限流算法,对于并发流量比较高的场景,其实可以用。但是在计数的过程中要进行资源数量的扣减和归还,这个其实在高并发的时候是多了一层逻辑处理的,而且这种限流是在在乎之前的流量处理结果。
如果并发场景消耗的资源比较少其实直接使用滑动窗口、令牌桶等限流算法更加适合,也就是只用于高并发和消耗内存、cpu少的场景,其次这种不能进行分布式的限流,因为他的令牌桶是基于本地内存。
二、RateLimiter使用
这里采用的是谷歌的RateLimiter作为底层限流封装,是谷歌基于令牌桶限流算法进行封装的工具类,但是在业务开发过程中要考虑几个问题
1、封装的限流器初始化
2、限流器对应的限流事件
3、默认的限流速率
4、事件可能不需要限流了
第一个问题要有初始化方法
第二个问题要考虑不管是接口还是mq接收处理事件,不可能一个限流器给所有的接口和mq使用,所以需要将事件与限流器做一个键值对,这里博主使用了ConcurrentHashMap
第三个问题默认其实随便设置一个就好了,但是需要能改动,这里设置跟随配置进行重置
第四个问题要对事件是否使用限流器进行设置,默认使用
@Slf4j
@Component
public class RateLimiterControl {
public static int default_rate = 20;
public static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
public static ConcurrentHashMap<String, Boolean> useFlowMap = new ConcurrentHashMap<>();
public synchronized static void initRate(String key, Integer rate) {
RateLimiter rateLimiter = rateLimiterMap.get(key);
if (rateLimiter == null) {
//首次启动key对应的限流器
rateLimiter = RateLimiter.create(rate);
rateLimiterMap.put(key, rateLimiter);
useFlowMap.put(key, true);
log.info("create new RateLimiter key:{}, is {}", key, rate);
return;
}
if (rate != rateLimiter.getRate()) {
//参数变更
log.info("Reset RateLimiter, new Rate is {}", rate);
rateLimiter.setRate(rate);
}
}
/**
* 请求令牌桶
*
* @return 是否允许
*/
public static boolean tryAcquire(String key) {
if (!useFlowMap.containsKey(key)) {
initRate(key, default_rate);
}
if (!useFlowMap.get(key)) {
//如果这个key事件不使用限流器
return true;
}
return rateLimiterMap.get(key).tryAcquire();
}
//设置是否使用限流器
public static void setEnable(String key, Boolean value) {
useFlowMap.put(key, value);
}
@Value("${default.rate:200}")
public void setDefaultRate(Integer rate) {
default_rate = rate;
}
}
以mq作为例子,这里是以topic加event作为一个事件进行限流,以topic也可以,其实就是限流的维度到底要什么。
关键在于限流不通过的处理,这里使用的是停一下然后抛出异常,然后借助mq的重试机制继续处理。其实一共有几种:
1、直接return(这种需要消息是会很快进来,丢掉没关系的,一般是心跳之类)
2、等待一段时间之后抛出异常(适用于mq重试机制)
3、直接抛出异常(接口和mq都可以,看业务能不能接受)
4、等待一段时间之后递归调用(这种很不建议,最坏的情况下会递归栈不断加深导致oom栈溢出,或者在oom之前出现过多死循环,耗尽线程池资源)
@ConsumeTopic(topic = "TP_FEED", eventCode = "EC_FEEDBACK_STATUS_CHANGE", log = true)
public class FeedbackListener implements TopicListener<FeedbackEvent> {
/**
* 接收消息.
*/
public void onMessage(MonsterMessage<FeedbackEvent> message) throws Exception {
if (!RateLimiterControl.tryAcquire("TP_FEED_EC_FEEDBACK_STATUS_CHANGE")) {
Thread.sleep(10);
throw new Exception();
}
}
}
三、对比
工具 | 限流算法 | 是否支持集群限流 | 适用限流 |
---|---|---|---|
count_limit | 加权计数器 | 是 | CPU、内存、并发 需要防止资源的过度消耗,在乎之前流量的处理结果 |
RateLimiterControl | 令牌桶 | 否 | 并发 不在乎进入的请求处理结果 |