SpringBoot+Redis+Lua实现接口限流的示例代码
作者:月生_
序言
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。这篇文章围绕Radis和Lua脚本来实现接口的限流
1.导入依赖
Lua脚本其在Redis2.6及以上的版本就已经内置了,所以需要导入的依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2.配置Redis环境
依赖导入成功后,需要在项目当中配置Redis的环境,这是程序和Redis交互的重要步骤。 在SpringBoot项目的资源路径下找到application.yml配置文件,内容如下:
spring:
redis: host: 127.0.0.1
port: 6379
database: 0
password:
timeout: 10s
lettuce:
pool:
min-idle: 0 #连接池中的最小空闲连接数为 0。这意味着在没有任何请求时,连接池可以没有空闲连接。
max-idle: 8 #连接池中的最大空闲连接数为 8。当连接池中的空闲连接数超过这个值时,多余的连接可能会被关闭以节省资源。
max-active: 8 #连接池允许的最大活动连接数为 8。在并发请求较高时,连接池最多可以创建 8 个连接来满足需求。
max-wait: -1ms #当连接池中的连接都被使用且没有空闲连接时,新的连接请求等待获取连接的最大时间。这里设置为 -1ms,表示无限等待,直到有可用连接为止。
3.创建限流类型
我们既然需要对一个接口进行限流,那么就需要配置应该以何种规则进行限流,比如ip地址、地理位置限流等,我们这里以ip限流为例。创建限流枚举类:
public enum LimitType {
/** * 针对某一个ip进行限流 */
IP("IP") ;
private final String type;
LimitType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
4.创建限流注解
自定义限流类型完成以后,需要定义限流注解,然后在需要被限流访问的接口上添加上限流注解,结合AOP切面即可实现限流的操作。限流注解定义如下:
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/** * 限流类型 * @return */
LimitType limitType() default LimitType.IP;
/** * 限流key * @return */
String key() default "";
/** * 限流时间 * @return */
int time() default 60;
/** * 限流次数 * @return */
int count() default 100;
}
5.编写限流的Lua脚本
我这里是创建了一个.lua结尾的文件,并把文章放在了项目资源的根路径下,也可以不创建文件,而是使用文本字符串的方式来编写脚本内容(稍后说明)。Lua脚本内容如下:
local key = KEYS[1]
local time = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) > count then
return tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, time)
end
return tonumber(current)
说明:redis.call('incr', key)命令可以使在Lua脚本调用Redis中的命令,该行代码的意思是使缓存中Key所对应的value值自增,如果Redis中Key所对应的值超过了count (限流次数),则直接返回count数量,如果没有超过count数量,则使value值+1
6.配置RedisConfig
接下来,需要配置RedisConfig的内容,比如以哪种序列化方式来序列化Key和Value,以及脚本执行器。代码如下:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@Configuration public class RedisConfig {
/** * RedisTemplate配置 * * @param factory * @return */
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer serializer = new StringRedisSerializer(StandardCharsets.UTF_8);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(serializer);
template.setValueSerializer(serializer);
template.setHashKeySerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
/** * Redis Lua 脚本 * * @return */
@Bean DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
// 我这里是以资源文件的形式来加载的lua脚本
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
return script;
}
}
也可以使用字符串文本的形式来加载
@Bean
DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
// 也可以使用文本字符串的形式来加载Lua脚本
script.setScriptText("local key = KEYS[1]\n" +
"local time = tonumber(ARGV[1])\n" +
"local count = tonumber(ARGV[2])\n" +
"local current = redis.call('get', key)\n" +
"\n" +
"if current and tonumber(current) > count then\n" +
" return tonumber(current)\n" +
"end\n" +
"\n" +
"current = redis.call('incr', key)\n" +
"\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', key, time)\n" +
"end\n" +
"\n" +
"return tonumber(current)\n" +
"\n");
return script;
}
7.编写限流切面 RateLimitAspect
前面的步骤完成之后,到了最后一步,编写限流的注解的AOP切换,在切面中通过Redis调用Lua脚本来判断当前请求是否达到限流的条件,如果达到则为抛出错误,由全局异常捕获返回给前端
import com.example.luatest.annotition.RateLimiter;
import com.example.luatest.exception.IPException;
import lombok.extern.java.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
@Aspect
@Component //切面类也需要加入到ioc容器
public class RateLimitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitAspect.class);
private final RedisTemplate<String, Object> redisTemplate;
private final DefaultRedisScript<Long> limitScript;
public RateLimitAspect(RedisTemplate<String, Object> redisTemplate, DefaultRedisScript<Long> limitScript) {
this.redisTemplate = redisTemplate;
this.limitScript = limitScript;
}
@Before("@annotation(rateLimiter)")
public void isAllowed(JoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws IPException, InstantiationException, IllegalAccessException {
String ip = null;
Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) arg;
ip = request.getRemoteHost(); break;
}
}
LOGGER.info("ip:{}", ip);
if (ip == null) {
throw new IPException("ip is null");
}
//拼接redis建
String key = rateLimiter.key() + ip;
// 执行 Lua 脚本进行限流判断
List<String> keyList = Collections.singletonList(key);
Long result = redisTemplate.execute(limitScript, keyList, key, Integer.toString(rateLimiter.count()), Integer.toString(rateLimiter.time()));
LOGGER.info("result:{}", result);
if (result != null && result > rateLimiter.count()) {
throw new IPException("IP [" + ip + "] 访问过于频繁,已超出限流次数");
}
}
}
8.使用注解
最后在方法上使用该限流注解即可
import com.example.luatest.annotition.RateLimiter;
import com.example.luatest.enum_.LimitType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/rate")
public class RateController {
@RateLimiter(count = 100, time = 60, limitType = LimitType.IP) @GetMapping("/someMethod")
public void someMethod(HttpServletRequest request) {
// 方法的具体逻辑
}
}
总结:
这篇文章到这里就结束,总的来说具体思路比较简单,我们通过创建限流注解,定义限流次数和间隔时间,然后对该注解进行AOP切面,在切面当中调用Lua脚本来判断是否达到限流条件,如果达到就抛出错误,由全局异常捕获,没有则代码继续执行。
到此这篇关于SprinBoot + Redis +Lua 实现接口限流的示例代码的文章就介绍到这了,更多相关SprinBoot Redis Lua接口限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
