使用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