限流
当遇到某个请求量激增时,可能会倒是接口占用过多的服务器资源,使得其他请求响应时间过慢或超时,有可能导致服务器挂机。这时可能通过对请求进行限制,对于部分超时请求,快速返回失败;
限流的算法
计数器算法
在规定的时间内限制能通过的请求数,如果限制1秒内只能通过100个请求,那在1秒内,每个请求都使数量加1,累计达到100次后,后续请求都被拒绝;一秒后,计数器重置为零,重新计数
漏桶算法
水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率
令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。
RateLimiter简介
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到.
创建一个RateLimiter ,设置速率为每秒生成 5 个令牌, 那么桶的最大值也为 5
RateLimiter rateLimiter = RateLimiter.create(5)
RateLimiter 提供了一些方法用于帮助判断
| 方法 | 作用 |
|---|---|
| setRate(double permitsPerSecond) | 设置每秒向桶中放入令牌的数量 |
| accquire() | 获取一个令牌,阻塞直到该请求可以为止,返回花费的时间 |
| accquire(int permits) | 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间 |
| tryAcquire() | 判断时候能获取到令牌, 如果不能获取立即返回 false |
| tryAcquire(int permits) | 判断时候能获取到n个令牌, 如果不能获取立即返回 false |
| tryAcquire(long timeout,TimeUnit unit) | 能否在指定时间内获取令牌,不能则返回false |
| tryAcquire(int permits,long timeout,TimeUnit unit) | 能否在指定时间和单位内获取令牌,不能则返回false |
RateLimit实例
在pom中添加
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
定义一个注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/**
* 每秒向桶放入令牌的数量
*/
double perSecond() default Double.MAX_VALUE;
/**
* 获取令牌的等待时间
*
* @return
*/
int timeOut() default 0;
/**
* 超时时间单位
*
* @return
*/
TimeUnit timeOutUnit() default TimeUnit.MILLISECONDS;
}
通过Aop判断是否使用注解@RateLimit和限流
@Log4j2
@Aspect
@Configuration
public class RateLimitAspect {
private RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
@Around("execution(public * *(..)) && @annotation(com.yuan.redis.authorization.RateLimit)")
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(RateLimit.class)) {
RateLimit rl = method.getAnnotation(RateLimit.class);
rateLimiter.setRate(rl.perSecond());
if (!rateLimiter.tryAcquire(rl.timeOut(), rl.timeOutUnit())) {
throw new ApiException("访问人数太多,请稍后再试试", ApiConstants.ERROR100600);
}
}
return pjp.proceed();
}
}
在接口上加注解@RateLimit
@ApiOperation(value = "测试限流", notes = "测试限流")
@RequestMapping(value = "/testRateLimiter.json", method = RequestMethod.POST)
@ApiResponses({@ApiResponse(code = 5000001, message = "参数错误")})
@RateLimit(perSecond =100, timeOut = 100)
public Result<Test> testRateLimiter() {
return Result.jsonStringOk();
}
通过jmeter 启用500线程压测,发现有部分不成功,加上@RateLimit(perSecond =100, timeOut = 100)后吞吐量降低了。
Redis Lua 实现限流
RateLimit是单机版的实现,接下来使用Redis Lua 实现简单的分布式限流(计数器算法实现),Redis Lua 代码如下
/**
* 通过KEYS[1]拿到value(不存在则赋值0),该值+1判断是否大于请求的最大次数,大于则加一并返回-1(表明已经被限流),小于则加一并设置过期为传入KEYS[3]
*/
public static String limit = "local key=KEYS[1]\n " +
"local limit=tonumber(KEYS[2])\n" +
"local num = tonumber(redis.call('get', key) or \"0\")\n" +
"if num+1 > limit then \n" +
" redis.call(\"INCRBY\", key,\"1\") \n" +
" return -1 \n" +
"else \n" +
" redis.call(\"INCRBY\", key,\"1\")\n" +
" redis.call(\"expire\", key,KEYS[3]) \n" +
" return num \n" +
"end";
java 代码,如果我们设置limitNum为100,expireTime为1,则表示单位时间1秒内只能有100个访问通过
public class LimitServiceImpl implements LimitService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Long limit(String key, int limitNum, int expireTime) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(LuaScript.limit);
List<String> keyList = new ArrayList();
keyList.add(key);
keyList.add(limitNum + "");
keyList.add(expireTime + "");
Long result = stringRedisTemplate.execute(redisScript, keyList);
return result;
}
}
在代码上实现限流也可以通过注解形式实现,和RateLimiter 和Spring Aop实现限流差不多。
在设置limit为50,expireTime为1,在jmeter下进行1秒内100个线程的访问,请求的前49个和第51个成功返回成功,其他返回错误。
如果需要用户对某个接口的访问次数,可以把key设置为该用户的唯一标识。