使用redis+lua腳本實現分佈式接口限流

問題描述  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

相關文章
相關標籤/搜索