接口限流实现
限流
限流是什么?韩国首尔梨泰院踩踏事件,一时刻大量人聚集在一个狭窄路口,最后导致事故的发生。假如果,进去的时候限流,出去的时候限流,严格管理,那么悲剧发生的概率是不是会小一点。
先问俩件事:
你的接口能支持多少qps?
假如100000个请求同时打在你的接口上,你的服务会发生什么事?
接口限流就是做力所能及的事,保证接口不被冲烂,超过阈值的请求,存在队列或者丢弃返回。
上图水龙头可以自己轻松设置水流的速度。
限流算法有哪些?
固定窗口
就是将时间划分为窗口模式,在一个窗口范围内,请求的次数的上限是固定的,超过窗口范围的请求便被丢弃。
当请求在窗口后半段,与下一个窗口的前半段发生时,qps
可能会翻倍。
滑动窗口
滑动窗口按时间再细分,每次都计算以当前时间为终止时间,然后时间精度(1s等精度)内的请求总数。解决了,qps
翻倍的问题。
漏桶
就是使用一个桶存储所有请求,然后桶会有一定流速流出消耗桶内任务,当桶内任务满的时候,选择抛弃任务。
令牌桶
就是使用一个桶存储所有令牌,然后桶会有一定流速生成令牌,当桶内任务满的时候,选择抛弃生成的令牌。请求接口时需要到令牌桶获取令牌。
限流算法实战
单机限流
Google Guava 利用令牌桶算法实现了限流工具类 RateLimiter。
- 引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
- 创建限流注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
@AliasFor("qps")
double value() default 0;
@AliasFor("value")
double qps() default 0;
int timeout() default 0;
TimeUnit timeUnit() default TimeUnit.MICROSECONDS;
}
- 进行拦截
/**
* @author tao
* @Date 2023-04-02
*/
@Aspect
@Component
public class RateLimitAspect {
/**
* RateLimiter Cache
* different interfaces have different rate limiters
*/
private ConcurrentHashMap<String, RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.tao.anno.RateLimit)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method method = methodSignature.getMethod();
// rate limiter cache key -> package name + method name (eg:"com.alibaba.comtroller.getUser")
String key = methodSignature.getDeclaringTypeName() + "." + method.getName();
RateLimit rateLimit = AnnotationUtils.findAnnotation(method, RateLimit.class);
if (rateLimit != null) {
double value = rateLimit.value();
int timeout = rateLimit.timeout();
TimeUnit timeUnit = rateLimit.timeUnit();
if (RATE_LIMITER_CACHE.get(key) == null) {
synchronized (key) {
if (RATE_LIMITER_CACHE.get(key) == null) {
RATE_LIMITER_CACHE.put(key, RateLimiter.create(value));
}
}
}
RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(key);
if (rateLimiter == null || !rateLimiter.tryAcquire(timeout, timeUnit)) {
// Custom processing logic, such as throwing exceptions
System.out.println(method.getName() + "interface rate limit");
}
}
return point.proceed();
}
}
- 使用
@RestController
@RequestMapping("/user")
public class RateLimitController {
@RateLimit(value = 1, timeout = 10, timeUnit = TimeUnit.MILLISECONDS)
@GetMapping("/limit1")
public String test1() {
return "Hello,world!";
}
@RateLimit(qps = 10, timeout = 10, timeUnit = TimeUnit.MILLISECONDS)
@GetMapping("/limit2")
public String test2() {
return "Hello,world!";
}
}
- 测试
接口请求速度过快,便会出现提示
分布式限流
- 限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonRateLimit {
//限流时存放在redis中key值
String redisKey();
//限流接口名
String interfaceName();
//在限流的时间内,允许通过的次数
long rate();
//限流的时间,单位毫秒
long rateInterval();
boolean isEnable() default false;
}
- AOP拦截
/**
* RRateLimiter API: https://www.javadoc.io/doc/org.redisson/redisson/3.10.6/org/redisson/api/RRateLimiter.html
*/
@Slf4j
@Aspect
public class RedissonRateLimitAspect {
@Autowired
private RedissonClient redissonClient;
@Before("@annotation(redissonRateLimit)")
public void redissonRateLimitHandle(RedissonRateLimit redissonRateLimit) {
if (!redissonRateLimit.isEnable() || ObjectUtil.isEmpty(redissonRateLimit) || ObjectUtil.isEmpty(redissonClient)) {
return;
}
//获取限流次数/限流时间/限流的key
String interfaceName = redissonRateLimit.interfaceName();
long rate = redissonRateLimit.rate();
long rateInterval = redissonRateLimit.rateInterval();
String redisKey = redissonRateLimit.redisKey();
RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
if (!rateLimiter.isExists()) {
rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.MILLISECONDS);
} else {
//获取旧限流的配置信息,防止服务重启,限流配置更新
RateLimiterConfig rateLimiterConfig = rateLimiter.getConfig();
Long oldRateInterval = rateLimiterConfig.getRateInterval();
Long oldRate = rateLimiterConfig.getRate();
if (rateInterval != oldRateInterval && rate != oldRate) {
//删除之前的限流配置
rateLimiter.delete();
rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.MILLISECONDS);
}
}
log.info("获取到的限流接口名:{},限流间隔时间:{},限流次数:{},限流的主键key:{}", interfaceName, rateInterval, rate, redisKey);
if (rateLimiter.getConfig() != null && !rateLimiter.tryAcquire()) {
throw new RedissonRateLimitException("当前请求次数过多,请在" + ((rateInterval / 1000 / 60) < 1 ? 1 :
(rateInterval / 1000 / 60 + 1)) + "分钟后尝试!");
}
}
}
- 测试
@RestController
@RequestMapping("/test")
public class TestController {
// 2000ms 生成1个令牌
@RedissonRateLimit(redisKey = "test", rateInterval = 2000, rate = 1, isEnable = true, interfaceName = "test")
@GetMapping("")
String test() {
return "success";
}
}
分布式限流器的原理:
- 记录限流器的配置:多长时间产生多少令牌
- 判断令牌是不是大于所需要的令牌,如果小于返回需要等待时间。
每次请求时会根据过期时间,清除已分配令牌。
参考文章
分布式服务限流实战,已经为你排好坑了:https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673
Rediss RRateLimiter 原理:https://blog.csdn.net/promisessh/article/details/112767743
Redisson分布式限流RRateLimiter的实现原理:https://blog.csdn.net/qq_41625866/article/details/129501114
Redisson官网中文文档:https://github.com/redisson/redisson/wiki/目录
本文来自博客园,作者:帅气的涛啊,转载请注明原文链接:https://www.cnblogs.com/handsometaoa/p/17281570.html