今天讲的 redis+lua 解决分布式限流 任何框架都能用,只要能集成 redis就可以,不管是微服务 dubbo、springcloud,还是直接用 springboot或者 springMVC都通用的方法。
前面我们已经讲了三篇关于 网关做限流的解决方案了,可查看链接
以上基于网关做限流操作,除了在 class 里面配置点东西,还需要在 yml 文件写配置,所以我这次使用 redis+lua 做限流,只需要在配置文件写你的 redis 配置就好了, 剩下的都交给 java 来处理。
东西好不好,大家往下看就清楚了,并且直接拿到你们项目里用就ok。
介绍一下本次使用所有框架和中间件的版本
框架 |
版本 |
---|---|
Spring Boot |
2.0.3.RELEASE |
Spring Cloud |
Finchley.RELEASE |
redis |
redis-4.0.11 |
JDK |
1.8.x |
父pom
ch3-4-eureka-client
ch3-4-eureka-server
redis-tool
org.springframework.boot
spring-boot-starter-parent
2.0.3.RELEASE
UTF-8
UTF-8
1.8
Finchley.RELEASE
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-aop
首先我们创建一个本次核心的工程,这个工程完全可以是你们项目里公共工程的其中一个文件夹,但在我的 Demo 中我这个重要的工程起名叫 redis-tool。
我们看这个工程所用到的依赖均在 父pom 中,需要有 aop 和redis 的依赖。
接下来看一下 我使用的 Lua 脚本,以下内容复制到 该项目的 resources 目录下,起名 limit.lua 即可。
local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key,"1")
redis.call("expire", key,"2")
return current + 1
end
解释 Lua 脚本含义:
限流注解
我们自定义一个注解,用来其他服务做限流使用的。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义限流注解
*
* @author yanlin
* @version v1.3
* @date 2019-04-05 7:58 PM
* @since v8.0
**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "limit";
int time() default 5;
int count() default 5;
}
配置类
@Component
public class Commons {
@Bean
public DefaultRedisScript redisluaScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
//读取 lua 脚本
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
redisScript.setResultType(Number.class);
return redisScript;
}
@Bean
public RedisTemplate limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate template = new RedisTemplate();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
拦截器
/**
* 拦截器
* @author yanlin
* @version v1.3
* @date 2019-04-05 8:06 PM
* @since v8.0
**/
@Aspect
@Configuration
public class LimitAspect {
private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);
@Autowired
private RedisTemplate limitRedisTemplate;
@Autowired
private DefaultRedisScript redisluaScript;
@Around("execution(* cn.springcloud.book.controller ..*(..) )")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Class> targetClass = method.getDeclaringClass();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit != null) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ipAddress = getIpAddr(request);
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(ipAddress).append("-")
.append(targetClass.getName()).append("- ")
.append(method.getName()).append("-")
.append(rateLimit.key());
List keys = Collections.singletonList(stringBuffer.toString());
Number number = limitRedisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());
if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
logger.info("限流时间段内访问第:{} 次", number.toString());
return joinPoint.proceed();
}
} else {
return joinPoint.proceed();
}
//由于本文没有配置公共异常类,如果配置可替换
throw new RuntimeException("已经到设置限流次数");
}
private static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}
代码解释:
拦截器 拦截 @RateLimit
注解的方法,使用Redsi execute
方法执行我们的限流脚本,判断是否超过限流次数,
我们这里 execution 参数 在你们实际项目中需要变更,一半都会定位到你们 controller层。
该工程在项目中就是你们需要做限流的服务,我们创建 一个maven 项目 ,端口设定8081,然后下面是 我 redis 的配置
redis 配置
spring:
# Redis数据库索引
redis:
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接池最大连接数(使用负值表示没有限制)
jedis:
pool:
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 10000
pom 文件
cn.springcloud.book
ch3-4
0.0.1-SNAPSHOT
../pom.xml
UTF-8
UTF-8
1.8
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
redis-tool
redis-tool
0.0.1-SNAPSHOT
控制层
我们在 controller 层的方法上加上我们的自定义限流注解,可以按照我们的默认值,也可以自定义参数。
@RestController
public class LimitController {
@RateLimit(key = "test", time = 10, count = 10)
@GetMapping("/test/limit")
public String testLimit() {
return "Hello,ok";
}
@RateLimit()
@GetMapping("/test/limit/a")
public String testLimitA() {
return "Hello,ok";
}
}
启动类
有一个关键的地方,我们需要引用 redis-tool 中的包,但是利用@ComponentScan 注解也会排除本工程的包,所以,这里我们要写上本工程包 和redis-tool中的包,一会在下面看我工程结构就明白了。
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(value = {"com.annotaion", "cn.springcloud", "com.config"})
public class Ch34EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(Ch34EurekaClientApplication.class, args);
}
}
测试
我们最好把 redis-tool 项目 install 一下,然后启动 eureka-client 工程,访问 http://localhost:8081/test/limit/a 5秒中超过5次访问 页面和后端就会报错。
当然我们不能说流量被限制了给用户一个500吧,所以我们还需要对后端对报错进行一个统一拦截,springboot 统一异常处理我已经出过文章了, 请点击 https://blog.csdn.net/weixin_38003389/article/details/83149252 进行配置,这样就可以友好对输出到前端。
本文参考文献:https://blog.csdn.net/yanpenglei/article/details/81772530
作者叫磊哥,加我微信可以联系到本人哦