系统限流主要有两张方式 一个就是令牌桶算法,另一个就是滑动窗口,
1,令牌桶算法。
这个原理说起来也比较简单,凡是访问系统的用户都需要一个令牌,这个令牌存放在一个固定的地方,系统会自动按照一定的速率往里面发放令牌,比如每秒钟100个。也就限制了每秒钟的访问量在100个左右。
a,这个算法在google的guava工具包里面已经实现了,demo代码也比较简单:
RateLimiter smoothBurstyRateLimiter = RateLimiter.create(2);// 平滑限流器,每秒生成令牌数量 2
RateLimiter smoothWarmingUpRateLimiter = RateLimiter.create(2, 10, TimeUnit.SECONDS);// 预热限流器,每秒生成令牌数量 2,预热时间 10s
smoothBurstyRateLimiter.tryAcquire();// 尝试获取令牌,获取不到就立即返回false
smoothBurstyRateLimiter.acquire();// 获取令牌,获取不到就一直阻塞等待
smoothBurstyRateLimiter.setRate(10);//还可以手动调整速率
经过测试 该算法会缓存速率两倍左右的令牌,即 每秒钟生成100个令牌的话,最多可以缓存200个左右的令牌。
b,实际应用,
对于系统限流可以限制整个系统的流量,利用AOP实现也比较简单。代码如下
@Aspect
@Component
public class GuavaRateLimitAspect {
/**
- 定义全局的 rateLimiter 每秒钟最多1000 请求
*/
private static final RateLimiter RATE_LIMITER = RateLimiter.create(1000);
/**
- 定义切点 增强所有的controller方法
/
@Pointcut("execution(public String com.kuafu.controller..*(..))") // 修饰符 返回值 方法名 参数
public void globalLimiter() { }
@Around(value = "globalLimiter()")
public Object aroundGlobalLimiter(ProceedingJoinPoint pjp) throws Throwable {
if(!RATE_LIMITER.tryAcquire()){
return "服务器繁忙,请稍后再试! 全局限流器执行限制";
}
return pjp.proceed();
}
}
还可以限制某个方法的流量,代码如下:
定义一个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GuavaRateLimit {
/**
*
- @return
*/
String value() default "";
/**
- 每秒向桶中放入令牌的数量 默认最大即不做限流
*/
double permitsPerSecond() default Double.MAX_VALUE;
/**
- 获取令牌的等待时间 默认0
*/
int timeOut() default 0;
/**
- 超时时间单位
*/
TimeUnit timeOutUnit() default TimeUnit.MILLISECONDS;
RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
}
定义AOP切面:
@Aspect
@Component
public class GuavaRateLimitAspect {
/**
- 定义切点 带有指定注解切入单个方法限流
*/
@Pointcut("@annotation(com.kuafu.rateLimiter.GuavaRateLimit)")
public void methodLimiter() { }
/**
- 环绕增强
*/
@Around(value = "methodLimiter()")
public String aroundMethodLimiter(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
//获取目标方法
Method targetMethod = methodSignature.getMethod();
if (targetMethod.isAnnotationPresent(GuavaRateLimit.class)) {
//获取目标方法的@GuavaRateLimit注解
GuavaRateLimit lxRateLimit = targetMethod.getAnnotation(GuavaRateLimit.class);
lxRateLimit.rateLimiter.setRate(lxRateLimit.permitsPerSecond());
if (!lxRateLimit.rateLimiter.tryAcquire(lxRateLimit.timeOut(), lxRateLimit.timeOutUnit()))
return "服务器繁忙,请稍后再试!";
}
return (String) pjp.proceed();
}
}
之后将注解添加到需要限流的方法上就可以了。
以上是对guava令牌桶算法的整理,但是还没有结束,这个只支持单节点系统的限流,如果是集群分布式的话,就失去全局效果了。接下里利用redisson来改造一下。
demo:
RedissonClient redisson = Redisson.create(new Config());
RRateLimiter limiter = redisson.getRateLimiter("myLimiter");
// Initialization required only once.
// 5 permits per 2 seconds
limiter.trySetRate(RateType.OVERALL, 5, 1, RateIntervalUnit.SECONDS);
// acquire 3 permits or block until they became available
limiter.acquire(1);
limiter.tryAcquire();
全局:
@Aspect
@Component
public class RedissonRateLimitAspect {
private final static Logger logger = LoggerFactory.getLogger(RedissonRateLimitAspect.class);
/**
- 定义全局的 rateLimiter 每秒钟最多1000 请求
*/
@Resource
private RedissonClient redissonClient;
private static final String KEY = "k1:k1:k1";
/**
- 定义切点 增强所有的controller方法
/
@Pointcut("execution(public String com.kuafu.controller..*(..))") // 修饰符 返回值 方法名 参数
public void globalLimiter() {}
@Around(value = "globalLimiter()")
public Object aroundGlobalLimiter(ProceedingJoinPoint pjp) throws Throwable {
logger.info("全局限流器执行限制");
final RRateLimiter rateLimiter = redissonClient.getRateLimiter(KEY);
rateLimiter.setRate(RateType.PER_CLIENT,1000,1, RateIntervalUnit.SECONDS);
if(!rateLimiter.tryAcquire()){
return "服务器繁忙,请稍后再试! 全局限流器执行限制";
}
return pjp.proceed();
}
}
单个方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonRateLimit {
/**
*
- @return
*/
String key() default "";
/**
- 每秒向桶中放入令牌的数量 默认最大即不做限流
*/
long permitsPerSecond() default Long.MAX_VALUE;
/**
- 获取令牌的等待时间 默认0
*/
int timeOut() default 0;
/**
-
超时时间单位
*/
TimeUnit timeOutUnit() default TimeUnit.MILLISECONDS;
}
@Aspect
@Component
public class RedissonRateLimitAspect {
/**
- 定义切点 带有指定注解切入单个方法限流
*/
@Pointcut("@annotation(com.kuafu.rateLimiter.RedissonRateLimit)")
public void methodLimiter() { }
@Around(value = "methodLimiter()")
public String aroundMethodLimiter(ProceedingJoinPoint pjp) throws Throwable {
logger.info("Limiter,method:{}...", pjp.getSignature().getName());
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
//获取目标方法
Method targetMethod = methodSignature.getMethod();
if (targetMethod.isAnnotationPresent(RedissonRateLimit.class)) {
//获取目标方法的@LxRateLimit注解
RedissonRateLimit lxRateLimit = targetMethod.getAnnotation(RedissonRateLimit.class);
final RRateLimiter rateLimiter = redissonClient.getRateLimiter(lxRateLimit.key());
rateLimiter.setRate(RateType.PER_CLIENT,lxRateLimit.permitsPerSecond(),1, RateIntervalUnit.SECONDS);
if (!rateLimiter.tryAcquire(lxRateLimit.timeOut(), lxRateLimit.timeOutUnit()))
return "服务器繁忙,请稍后再试!";
}
return (String) pjp.proceed();
}
}