使用redis+lua脚本实现分布式接口限流

被刻印的时光 ゝ 提交于 2020-01-07 03:52:02

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

问题描述  

  某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。 

 

一、限流总并发/连接/请求数

对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。

如果你使用过Tomcat,其Connector 其中一种配置有如下几个参数:

acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;

maxConnections: 瞬时最大连接数,超出的会排队等待;

maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死。

详细的配置请参考官方文档。另外如Mysql(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置。

二、控制访问速率

   在工程实践中,常见的是使用令牌桶算法来实现这种模式,常用的限流算法有两种:漏桶算法和令牌桶算法。

   https://blog.csdn.net/fanrenxiang/article/details/80683378(漏桶算法和令牌桶算法介绍 传送门)

 

三、分布式限流

分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用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";
    }
四、参考文章

https://jinnianshilongnian.iteye.com/blog/2305117

 https://blog.csdn.net/fanrenxiang/article/details/80683378

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!