令牌桶算法:
生产逻辑:程序以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,无法再向桶中放置令牌;
消费逻辑:当想要处理一个请求的时候,则从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝处理请求。
优点:既能限制数据的平均传输速率,又能允许某种程度的突发传输;
算法实现:
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.14.1</version>
</dependency>
令牌桶工具类
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.SneakyThrows;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* 按数据库配置对其进行限流
*/
@Component
public class RedisRateLimiter {
@Autowired
private RedissonClient redissonClient;
// ratelimiter缓存容器
private static Cache<String, Optional<RRateLimiter>> rrLimiterCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
/**
* 通用法
*
* @param key
* @return
*/
public boolean acquire(String key, int num, int interval) {
//acquire
RRateLimiter rrLimiter = getRrLimiter("RateLimiter_" + key, num, interval);
return rrLimiter.tryAcquire();
}
/**
* 获取流限器
*
* @param key
* @param num 令牌数
* @param interval 时间窗口大小
* @return
*/
@SneakyThrows
private RRateLimiter getRrLimiter(String key, int num, int interval) {
//如不存在相应的limiter,进行新建
return rrLimiterCache.get(key, new Callable<Optional<RRateLimiter>>() {
@Override
public Optional<RRateLimiter> call() {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.setRate(RateType.OVERALL, num, interval, RateIntervalUnit.SECONDS);
//如果不存在,进行放置
return Optional.of(rateLimiter);
}
}).get();
}
@SneakyThrows
public RRateLimiter getFowIdLimit(String key) {
//如不存在相应的limiter,进行新建
return rrLimiterCache.get(key, new Callable<Optional<RRateLimiter>>() {
@Override
public Optional<RRateLimiter> call() {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 1秒一个
rateLimiter.setRate(RateType.OVERALL, 5, 1, RateIntervalUnit.SECONDS);
//如果不存在,进行放置
return Optional.of(rateLimiter);
}
}).get();
}
}
具体使用:
// 获取到命名为key的令牌桶
RRateLimiter rateLimiter = redisRateLimiter.getFowIdLimit(key);
// 判断是否能够取到令牌
if (rateLimiter.tryAcquire()) {
// 处理请求
sendMessage(webHookRequest, flowDefinitions, appID, customerID, eventType);
} else {
// 拒绝请求
saveHookUrl(webHookRequest, flowDefinitions, appID, customerID,
eventType, StatusCodeEnum.LIMITED.getCode());
}
可以看出,单机环境和分布式限流器的不同在于:对令牌桶的实现方法不同。单机环境下所有的令牌都生成在内存中,是JVM级别的限流;当令牌生产方式变为redis,便做到了分布式环境下的限流。
学习心得:
根据令牌桶算法,桶中的令牌是持续生成存放的,需要先从桶中拿到令牌才能开始执行请求,那么持续生成令牌存放应该这么实现呢?
一般实现:开启一个定时任务,由定时任务持续生成令牌。
缺点:每维护一个令牌桶都需要创建一个任务,会极大的消耗系统资源。如某接口需要分别对每个用户做访问频率限制,假设系统中存在100W用户,则可能需要开启100W个定时任务来维持每个桶的令牌数。
RRateLimiter实现:延迟计算法,具体函数如下。
/**
* Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time
*/
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
基于当前时间,更新下一次请求令牌的时间,以及当前存储的令牌(即生成令牌),这样一来,只需要在获取令牌时计算一次即可。