工作中对外提供的API 接口设计很多时候要考虑限流,如果不考虑,可能会造成系统的连锁反应,轻者响应缓慢,重者系统宕机。而常用的限流算法有令牌桶算法和漏桶算法,本篇介绍令牌桶算法
令牌桶算法
原理如上图,系统以恒定速率不断产生令牌,令牌桶有最大容量,超过最大容量则丢弃,同时用户请求接口,如果此时令牌桶中有令牌则能访问获取数据,否则直接拒绝用户请求
java代码实现
/**
* 线程池每0.5s发送随机数量的请求,每次请求计算当前的令牌数量,请求令牌数量超出当前令牌数量,则产生限流
*/
@Slf4j
public class TokensLimiter {
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
// 最后一次令牌发放时间
public long timeStamp = System.currentTimeMillis();
// 桶的容量
private int capacity = 7;
// 令牌生成速度5/s
private int rate = 5;
// 当前令牌数量
private int tokens;
public void acquire() {
//令牌生成速度 = 5/1s 此次时间-上次生成时间=中间耗费时间
scheduledExecutorService.scheduleWithFixedDelay(() -> {
long now = System.currentTimeMillis();
long tokensCal = tokens + (now - timeStamp) * rate/1000;
int tokenCalInt = (int)tokensCal;
// 当前令牌数
tokens = Math.min(capacity,tokenCalInt);
//每隔0.5秒发送随机数量的请求
int permits = (int) (Math.random() * 9) + 1;
log.info("请求令牌数:" + permits + ",当前令牌数:" + tokens);
timeStamp = now;
if (tokens < permits) {
// 若不到令牌,则拒绝
log.info("限流了");
} else {
// 还有令牌,领取令牌
tokens -= permits;
log.info("剩余令牌=" + tokens);
}
}, 1000, 500, TimeUnit.MILLISECONDS);
//1秒以后开始执行第一次任务,第一次执行完每隔500ms执行下次任务
}
public static void main(String[] args) {
TokensLimiter tokensLimiter = new TokensLimiter();
tokensLimiter.acquire();
}
}
输出结果:
Guava rateLimiter实现
public abstract class AbstractInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ResponseEnum result;
try{
result = preFilter(request);
}catch (Exception e){
result = ResponseEnum.SERVER_ERROR;
}
if(ResponseEnum.OK == result){
return true;
}
//未申请到被限流
rateLimitResponse(result,response);
return false;
}
private void rateLimitResponse(ResponseEnum result, HttpServletResponse response){
R r = R.error(500,result.getMsg());
try {
response.getWriter().write(JSON.toJSONString(r));
} catch (IOException e) {
e.printStackTrace();
}
}
//自己声明的抽象方法,交给子类实现
protected abstract ResponseEnum preFilter(HttpServletRequest request);
}
@Slf4j
@Component
public class RateLimitInterceptor extends AbstractInterceptor {
/**
* 单机全局限流器,QPS为1
*/
@SuppressWarnings("UnstableApiUsage")
private static final RateLimiter RATE_LIMITER = RateLimiter.create(1);
@Override
protected ResponseEnum preFilter(HttpServletRequest request) {
if(!RATE_LIMITER.tryAcquire()){
log.info("限流了..");
return ResponseEnum.RATE_LIMIT;
}
log.info("请求成功");
return ResponseEnum.OK;
}
}
@Getter
public enum ResponseEnum {
OK("成功"),RATE_LIMIT("访问次数受限"),SERVER_ERROR("服务器错误"),QUERY_FAIL("查询失败");
private String msg;
ResponseEnum(String msg) {
this.msg = msg;
}
}
执行结果
Redis rateLimiter
分布式环境下解决方案
需要限流的接口使用该注解
/**
* 1(时间)分钟(单位)允许某个ip请求的最大次数(max)
*
* 如每隔2分钟,单IP限定访问次数不能超过10次
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisRateLimiter {
/**
* 默认根据IP拦截
*/
LimitType limitType() default LimitType.IP;
enum LimitType{
GENERAL,IP,USERID;
}
/**
* 限制时间长度
*/
long timeLimitLength() default 1;
/**
* 限制时间长度的单位
*/
TimeUnit timeLimitLengthUnit() default TimeUnit.SECONDS;
/**
* 允许时间内最大访问数
*/
long max() default 1;
/**
* redis存储的key
*/
String storeKey() default "";
}
redis配置文件
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("unchecked")
public RedisScript<Long> limitRedisScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/redis/limit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}
切面
@Slf4j
@Component
@Aspect
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RedisRateLimitAspect {
private final static String REDIS_RATE_LIMIT_KEY_PREFIX="limit:";
private final StringRedisTemplate stringRedisTemplate;
private final RedisScript<Long> limitRedisScript;
@Pointcut("@annotation(com.jerrysong.jwt.annotations.RedisRateLimiter)")
public void rateLimit() {}
@Before("rateLimit()")
public void pointCut(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
RedisRateLimiter redisRateLimit = AnnotationUtils.findAnnotation(method, RedisRateLimiter.class);
if(redisRateLimit!=null){
//获取存储key名称
String key = redisRateLimit.storeKey();
//获取时间限制
long timeLimitLength = redisRateLimit.timeLimitLength();
//获取时间限制单位
TimeUnit timeLimitLengthUnit = redisRateLimit.timeLimitLengthUnit();
//时间单位最大访问数目
long max = redisRateLimit.max();
if(StringUtils.isBlank(key)){
key = method.getDeclaringClass().getSimpleName()+"."+method.getName();
}
HttpServletRequest request
= ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
key = key+":"+ IpUtil.getIpAddr(request);
//追加统一限流前缀
key =REDIS_RATE_LIMIT_KEY_PREFIX +key;
long now = System.currentTimeMillis();
//将2分钟转化为毫秒时间戳,以获得2分钟前时间
long limitTimeLengthMills =timeLimitLengthUnit.toMillis(timeLimitLength);
//应该移除的分值区间
long removeScore = now-limitTimeLengthMills;
Long r = stringRedisTemplate.execute(
limitRedisScript,
Lists.newArrayList(key),
"" + now,
"" + limitTimeLengthMills, //设置key的保存时间,该key在2分钟的允许时间内做zadd操作
"" + removeScore, //移除当前时间2分钟前过期的score
"" + max);
if(r!=null){
if(r==0){
log.error("【{}】在 "+timeLimitLength+formatTimeUnit(timeLimitLengthUnit)+" 内已达到访问上限,当前接口上限 {}", key, max);
throw new RuntimeException("手速太快了,慢点儿吧~");
}else{
log.info("【{}】在 "+timeLimitLength+formatTimeUnit(timeLimitLengthUnit)+" 内访问 {} 次", key, r);
}
}
}
}
private String formatTimeUnit(TimeUnit timeUnit){
if(timeUnit==TimeUnit.MINUTES){
return "分钟";
}else if(timeUnit==TimeUnit.SECONDS){
return "秒";
}else if(timeUnit==TimeUnit.HOURS){
return "小时";
}
return "illegal timeUnit args";
}
}
LUA脚本
local key = KEYS[1]
local now = tonumber(ARGV[1])
local limitTimeLengthMills = tonumber(ARGV[2])
local removeScore = tonumber(ARGV[3])
local max = tonumber(ARGV[4])
redis.call('ZREMRANGEBYSCORE',key,0,removeScore)
local current = tonumber(redis.call('ZCARD',key))
local next = current+1
if next>max then
return 0
else
redis.call('ZADD',key,now,now)
redis.call('PEXPIRE',key,limitTimeLengthMills)
return next
end
控制层
@Slf4j
@RestController
public class RedisLimitController {
@TokenNoCheck
@RedisRateLimiter
@GetMapping("/test1")
public R test1() {
log.info("【test1】被执行了。。。。。");
return R.ok("成功访问到api [1]~");
}
@TokenNoCheck
@RedisRateLimiter(max = 1,limitType = RedisRateLimiter.LimitType.IP,timeLimitLength = 1,timeLimitLengthUnit = TimeUnit.SECONDS)
@GetMapping("/test2")
public R test2() {
log.info("【test2】被执行了。。。。。");
return R.ok("成功访问到api [2]~");
}
@TokenNoCheck
@RedisRateLimiter(max = 5,limitType = RedisRateLimiter.LimitType.IP,timeLimitLength = 1,timeLimitLengthUnit = TimeUnit.MINUTES)
@GetMapping("/test3")
public R test3() {
log.info("【test3】被执行了。。。。。");
return R.ok("成功访问到api [3]~");
}
}