高併發請求限流 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";
	}

 好了,到此結束,可以使用 測試工具自己測試下效果

 

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