問題描述 java
某天A君忽然發現本身的接口請求量忽然漲到以前的10倍,沒多久該接口幾乎不可以使用,並引起連鎖反應致使整個系統崩潰。如何應對這種狀況呢?生活給了咱們答案:好比老式電閘都安裝了保險絲,一旦有人使用超大功率的設備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理咱們的接口也須要安裝上「保險絲」,以防止非預期的請求對系統壓力過大而引發的系統癱瘓,當流量過大時,能夠採起拒絕或者引流等機制。 web
1、限流總併發/鏈接/請求數redis
對於一個應用系統來講必定會有極限併發/請求數,即總有一個TPS/QPS閥值,若是超了閥值則系統就會不響應用戶請求或響應的很是慢,所以咱們最好進行過載保護,防止大量請求涌入擊垮系統。算法
若是你使用過Tomcat,其Connector 其中一種配置有以下幾個參數:spring
acceptCount:若是Tomcat的線程都忙於響應,新來的鏈接會進入隊列排隊,若是超出排隊大小,則拒絕鏈接;sql
maxConnections: 瞬時最大鏈接數,超出的會排隊等待;併發
maxThreads:Tomcat能啓動用來處理請求的最大線程數,若是請求處理量一直遠遠大於最大線程數則可能會僵死。app
詳細的配置請參考官方文檔。另外如Mysql(如max_connections)、Redis(如tcp-backlog)都會有相似的限制鏈接數的配置。tcp
2、控制訪問速率分佈式
在工程實踐中,常見的是使用令牌桶算法來實現這種模式,經常使用的限流算法有兩種:漏桶算法和令牌桶算法。
https://blog.csdn.net/fanrenxiang/article/details/80683378(漏桶算法和令牌桶算法介紹 傳送門)
3、分佈式限流
分佈式限流最關鍵的是要將限流服務作成原子化,而解決方案可使使用redis+lua腳本,咱們重點來看看java代碼實現(aop)
lua腳本
private final String LUA_LIMIT_SCRIPT = "local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local current = tonumber(redis.call('get', key) or \"0\")\n" +
"if current + 1 > limit then\n" +
" return 0\n" +
"else\n" +
" redis.call(\"INCRBY\", key,\"1\")\n" +
" redis.call(\"expire\", key,\"2\")\n" +
" return 1\n" +
"end";
keys[1]傳入的key參數
ARGV[1]傳入的value參數(這裏指限流大小)
自定義註解的目的,是在須要限流的方法上使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {
/**
*
* @return
*/
String key();
/**
* 限流次數
* @return
*/
String count();
}
spring aop
package com.example.commons.aspect;
import com.example.commons.annotation.Limit;
import com.example.commons.exception.LimitException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
/**
* Created by Administrator on 2019/3/17.
*/
@Aspect
@Component
public class LimitAspect {
private final String LIMIT_PREFIX = "limit_";
private final String LUA_LIMIT_SCRIPT = "local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local current = tonumber(redis.call('get', key) or \"0\")\n" +
"if current + 1 > limit then\n" +
" return 0\n" +
"else\n" +
" redis.call(\"INCRBY\", key,\"1\")\n" +
" redis.call(\"expire\", key,\"2\")\n" +
" return 1\n" +
"end";
@Autowired
private RedisTemplate redisTemplate;
DefaultRedisScript<Number> redisLUAScript;
StringRedisSerializer argsSerializer;
StringRedisSerializer resultSerializer;
@PostConstruct
public void initLUA() {
redisLUAScript = new DefaultRedisScript<>();
redisLUAScript.setScriptText(LUA_LIMIT_SCRIPT);
redisLUAScript.setResultType(Number.class);
argsSerializer = new StringRedisSerializer();
resultSerializer = new StringRedisSerializer();
}
@Around("execution(* com.example.controller ..*(..) )")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Limit rateLimit = method.getAnnotation(Limit.class);
if (rateLimit != null) {
String key = rateLimit.key();
String limitCount = rateLimit.count();
List<String> keys = Collections.singletonList(LIMIT_PREFIX + key);
Number number = (Number) redisTemplate.execute(redisLUAScript, argsSerializer, resultSerializer, keys, limitCount);
if (number.intValue() == 1) {
return joinPoint.proceed();
} else {
throw new LimitException();
}
} else {
return joinPoint.proceed();
}
}
}
自定義異常
public class LimitException extends RuntimeException {
static final long serialVersionUID = 20190317;
public LimitException () {
super();
}
public LimitException (String s) {
super (s);
}
}
controllerAdvice
@org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {
@ExceptionHandler(LimitException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public @ResponseBody Map limitExceptionHandler() {
Map<String, Object> result = new HashMap();
result.put("code", "500");
result.put("msg", "請求次數已經到設置限流次數!");
return result;
}
}
控制層方法直接使用咱們本身定義的註解就能夠實現接口限流了
@Limit(key = "print", count = "2")
@GetMapping("/print")
public String print() {
return "print";
}
4、參考文章
https://jinnianshilongnian.iteye.com/blog/2305117
https://blog.csdn.net/fanrenxiang/article/details/80683378