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

问题描述
某天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]
" +
         "local limit = tonumber(ARGV[1])
" +
         "local current = tonumber(redis.call("get", key) or "0")
" +
         "if current + 1 > limit then
" +
         "   return 0
" +
         "else
" +
         "   redis.call("INCRBY", key,"1")
" +
         "   redis.call("expire", key,"2")
" +
         "   return 1
" +
         "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]
" +
             "local limit = tonumber(ARGV[1])
" +
             "local current = tonumber(redis.call("get", key) or "0")
" +
             "if current + 1 > limit then
" +
             "   return 0
" +
             "else
" +
             "   redis.call("INCRBY", key,"1")
" +
             "   redis.call("expire", key,"2")
" +
             "   return 1
" +
             "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
以上是 使用redis+lua脚本实现分布式接口限流 的全部内容, 来源链接: utcz.com/z/512316.html








