高并发请求限流 RateLimiter令牌桶限流

高并发限流解决方案

高并发限流解决方案限流算法(令牌桶、漏桶、计数器)、应用层解决限流(Nginx)

限流算法

常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。

计数器

  它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多,如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器

滑动窗口计数

滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。
滑动窗口原理是在每次有访问进来时,先判断前 N 个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数 +1。

令牌桶算法

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:

假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;

桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;

当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;

如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

 

使用RateLimiter实现令牌桶限流

RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。

通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。

下面来看一些简单的实践,需要先引入guava的maven依赖。

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.0.RELEASE</version>
	</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>25.1-jre</version>
		</dependency>
	</dependencies>
/*
 * 使用RateLimiter 实现令牌通方式限流
 */
@RestController
public class IndexController {
	@Autowired
	private OrderService orderService;
	// create 方法中传入一个参数 以每秒为单位固定的速率值 1r/s   这里100表示每秒中往桶中存入1 令牌
	RateLimiter rateLimiter = RateLimiter.create(1); // 独立线程

	// 相当于该接口每秒钟时间 只能支持一个客户端访问
	@RequestMapping("/addOrder")
	public String addOrder() {
		// 1.限流处理 限流正常要放在网关 客户端从桶中获取对应的令牌,为什么返回double结果,这个结果表示 从桶中拿到令牌等待时间.
		// 2. 如果获取不到令牌,就会一直等待.设置服务降级处理(相当于配置在规定时间内如果没有获取到令牌的话,直接走服务降级。)
		// double acquire = rateLimiter.acquire();
		//
		// System.out.println("从桶中获取令牌等待的时间:" + acquire);
		// 如果在500毫秒内如果没有获取到令牌的话,则直接走服务降级处理
		boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
		if (!tryAcquire) {
			System.out.println("别抢了, 怎么抢也是一直等待的, 还是放弃吧!!!");
			return "别抢了, 怎么抢也是一直等待的, 还是放弃吧!!!";
		}
		// 2. 业务逻辑处理
		boolean addOrderResult = orderService.addOrder();
		if (addOrderResult) {
			System.out.println("恭喜您,抢购成功!" );
			return "恭喜您,抢购成功!";
		}
		return "抢购失败!";
	}

}

上面这种方式,写起来比较麻烦,一旦多个接口要限流的话,代码复用性不好,可以使用aop技术进行封装一下

定义注解:

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtRateLimiter {
    //以每秒为单位固定指定的速率往令牌桶里添加令牌
	double value();

	//毫秒 表示 从桶中拿到令牌等待时间. 如果获取不到令牌 设置服务降级处理(相当于配置在规定时间内如果没有获取到令牌的话,直接走服务降级 )
	long timeOut();
}

 添加springboot——aop依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

aop实现类

@Aspect
@Component
public class RateLimiterAop {
	// 存放接口是否已经存在
	private static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<String, RateLimiter>();

	@Pointcut("execution(public * com.itmayeidu.api.*.*(..))")
	public void rlAop() {
	}

	@Around("rlAop()")
	public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
		// 使用Java反射技术获取方法上是否有@ExtRateLimiter注解类
		ExtRateLimiter extRateLimiter = signature.getMethod().getDeclaredAnnotation(ExtRateLimiter.class);
		if (extRateLimiter == null) {
			// 正常执行方法
			Object proceed = proceedingJoinPoint.proceed();
			return proceed;
		}
		// ############获取注解上的参数 配置固定速率 ###############
		// 获取配置的速率
		double value = extRateLimiter.value();
		// 获取等待令牌等待时间
		long timeOut = extRateLimiter.timeOut();
		RateLimiter rateLimiter = getRateLimiter(value, timeOut);
		// 判断令牌桶获取token 是否超时
		boolean tryAcquire = rateLimiter.tryAcquire(timeOut, TimeUnit.MILLISECONDS);
		if (!tryAcquire) {
			serviceDowng();
			return null;
		}
		// 获取到令牌,直接执行..
		Object proceed = proceedingJoinPoint.proceed();
		return proceed;

	}

	// 获取RateLimiter对象
	private RateLimiter getRateLimiter(double value, long timeOut) {
		// 获取当前URL
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = attributes.getRequest();
		String requestURI = request.getRequestURI();
		RateLimiter rateLimiter = null;
		if (!rateLimiterMap.containsKey(requestURI)) {
			// 开启令牌通限流
			rateLimiter = RateLimiter.create(value); // 独立线程
			rateLimiterMap.put(requestURI, rateLimiter);
		} else {
			rateLimiter = rateLimiterMap.get(requestURI);
		}
		return rateLimiter;
	}

	// 服务降级
	private void serviceDowng() throws IOException {
		// 执行服务降级处理
		System.out.println("执行降级方法,亲,服务器忙!请稍后重试!");
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletResponse response = attributes.getResponse();
		response.setHeader("Content-type", "text/html;charset=UTF-8");
		PrintWriter writer = response.getWriter();
		try {
			writer.println("执行降级方法,亲,服务器忙!请稍后重试!");
		} catch (Exception e) {

		} finally {
			writer.close();
		}

	}

	public static void main(String[] args) {
		// 使用Java反射技术获取方法上是否有@ExtRateLimiter注解类
		ExtRateLimiter extRateLimiter = IndexController.class.getClass().getAnnotation(ExtRateLimiter.class);
		System.out.println(extRateLimiter);
	}

}

在controller中给方法加上注解

// 使用注解方式实现服务令牌桶限流    value   0.5 表示每秒钟放入的令牌数    timeOut超时时间
	@RequestMapping("/findOrder")
	@ExtRateLimiter(value = 0.5, timeOut = 500)
	public String findOrder() throws InterruptedException {
		System.out.println("findOrder");
		return "SUCCESS";
	}
 
	// 使用注解方式实现服务令牌桶限流
	@RequestMapping("/myOrder")
	@ExtRateLimiter(value = 10.0, timeOut = 500)
	public String myOrder() throws InterruptedException {
		System.out.println("myOrder");
		return "SUCCESS";
	}

 好了,到此结束,可以使用 测试工具自己测试下效果

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章