限流策略的背景
在开发各种项目时,我们常常需要思考如何应对被自动化脚本或爬虫程序频繁请求而导致服务器资源过载的情况。为了应对这种情况,我们通常需要采用限流策略。然而,现有的限流方案可能存在一些问题,如灵活性较低、不易复用等。那么,有没有一种灵活性较高且可复用的方案呢?答案是肯定的,这里介绍若依框架中的@RateLimiter注解限流策略。通过在方法上添加@RateLimiter注解,我们可以灵活地定义限流规则,并且这种限流策略可以被多个方法复用。这个@RateLimiter注解可以根据需要设置不同的限流策略,如基于IP的限流、基于并发数的限流,从而对请求进行限制,保护服务器资源的稳定运行。使用若依框架的@RateLimiter注解,可以方便地实现灵活且可复用的限流策略,从而有效应对频繁请求导致的服务器资源过载问题。
配置Bean:RedisTemplate
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
RedisTemplate的作用
RedisTemplate是Spring Data Redis提供的一个用于操作Redis的工具类,它封装了对Redis的常见操作,提供了一系列的方法来方便开发者对Redis进行读取、写入、删除等操作。
主要功能包括:
1. 实现数据的序列化和反序列化:RedisTemplate可以配置Key和Value的序列化和反序列化策略,可以使用默认的JDK序列化方式,也可以使用其他的序列化方式,如JSON等。
2. 提供了常见的操作方法:RedisTemplate提供了一系列的方法,如set、get、delete等,用于存储、获取和删除数据。同时,还提供了对数据类型(如String、Hash、List、Set、ZSet等)的操作方法,如对Hash进行增删改查、对List进行添加和获取元素等。
3. 支持事务操作:RedisTemplate支持事务操作,可以将一系列的操作封装在一个事务中,保证这些操作的原子性。
4. 支持Lua脚本执行:RedisTemplate可以执行Lua脚本,可以通过执行脚本来实现一些复杂的操作。
总而言之,RedisTemplate是一个功能强大且易于使用的Redis操作工具类,使用RedisTemplate,可以方便地对Redis进行数据操作,并且可以灵活地配置序列化策略和执行事务操作。
为什么要重新配置RedisTemplate的序列化和反序列化?
如果不配置序列化和反序列化策略,RedisTemplate默认使用JDK自带的序列化方式,即使用JDK的ObjectOutputStream进行序列化,使用ObjectInputStream进行反序列化。这种方式的问题是,它会将对象序列化成字节流时,会包含一些额外的信息,如类名、字段名等等,导致存储在Redis中的值非常冗余。
因此,为了更高效地使用Redis存储对象,并且减少存储空间的占用,我们通常需要配置RedisTemplate的序列化和反序列化策略,例如使用JSON或者其他更紧凑的序列化方式。这样可以确保对象能够正确地序列化和反序列化存储到Redis中,并且可以在读取时正常还原成原始对象。
配置Bean:DefaultRedisScript
@Bean
public DefaultRedisScript<Long> limitScript()
{
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(limitScriptText());
redisScript.setResultType(Long.class);
return redisScript;
}
/**
* 限流脚本
*/
private String limitScriptText()
{
return "local key = KEYS[1]\n" +
"local count = tonumber(ARGV[1])\n" +
"local time = tonumber(ARGV[2])\n" +
"local current = redis.call('get', key);\n" +
"if current and tonumber(current) > count then\n" +
" return tonumber(current);\n" +
"end\n" +
"current = redis.call('incr', key)\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', key, time)\n" +
"end\n" +
"return tonumber(current);";
}
DefaultRedisScript的作用
DefaultRedisScript是Spring Data Redis提供的一个辅助类,用于执行Lua脚本。
DefaultRedisScript的作用包括:
1. 封装了Lua脚本的内容:DefaultRedisScript可以将Lua脚本的内容封装起来,并提供了一些方法来设置Lua脚本的参数。
2. 指定返回值的类型:DefaultRedisScript可以通过设置返回值的类型,来告诉Redis该如何解析Lua脚本执行后的返回结果。
3. 提供了执行脚本的方法:DefaultRedisScript提供了一个execute方法,用于执行Lua脚本。该方法可以与RedisTemplate结合使用,执行Lua脚本后获取返回结果。
总而言之,DefaultRedisScript是一个用于执行Lua脚本的辅助类,通过它可以方便地在Spring Data Redis中执行Lua脚本,并获取执行结果。
lua脚本通过计数器实现限流
- 接收参数:key,count,time
- 调用get方法获取key中的值current,如果这个key存在并且current大于count,返回current
- 调用redis的自增函数赋值给current,当current=1时(即第一次访问该接口),调用redis的设置过期时间函数给当前key设置过期时间
- 返回current
总体来说,这段Lua脚本的功能是实现一个简单的计数器,每次调用脚本时,会将指定键的值加1,并返回当前的计数值。如果计数值已经达到了指定的上限,则不会继续增加,直接返回当前的计数值。同时,如果计数值是从0开始的,则会设置键的过期时间为指定的时间。
@RateLimiter注解
/**
* 限流注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter
{
/**
* 限流key
*/
public String key() default CacheConstants.RATE_LIMIT_KEY;
/**
* 限流时间,单位秒
*/
public int time() default 60;
/**
* 限流次数
*/
public int count() default 100;
/**
* 限流类型
*/
public LimitType limitType() default LimitType.DEFAULT;
}
@Target(ElementType.METHOD)
@Target注解作用:指定该注解的作用域
ElementType枚举中各个属性的作用:
- TYPE:可以被应用在类、接口或枚举类型上。
- FIELD:可以被应用在字段上。
- METHOD:可以被应用在方法上。
- PARAMETER:可以被应用在方法的参数上。
- CONSTRUCTOR:可以被应用在构造方法上。
- LOCAL_VARIABLE:可以被应用在局部变量上。
- ANNOTATION_TYPE:可以被应用在注解类型上。
- PACKAGE:可以被应用在包上。
四个属性:
- key:存储在redis里的key,默认值:"rate_limit:"
- time:限流时间,默认值:60s
- count:限流数量,默认值:100
- limitType: 限流类型,默认值:全局
@Retention(RetentionPolicy.RUNTIME)
@Retention注解作用:用于指定注解的保留策略
RetentionPolicy枚举中各个属性的作用:
- SOURCE:注解仅存在于源代码中,编译时会被编译器忽略,不会包含在编译后的字节码中。
- CLASS:注解会被编译器保留在编译后的字节码文件中,但在运行时无法获取到。
- RUNTIME:注解会被保留在编译后的字节码文件中,并且可以在运行时通过反射机制获取到。
使用AOP技术自动注入限流功能
/**
* 限流处理
*
* @author ruoyi
*/
@Aspect
@Component
public class RateLimiterAspect
{
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
private RedisTemplate<Object, Object> redisTemplate;
private RedisScript<Long> limitScript;
@Autowired
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
{
this.redisTemplate = redisTemplate;
}
@Autowired
public void setLimitScript(RedisScript<Long> limitScript)
{
this.limitScript = limitScript;
}
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
{
// 获取注解的限流时间
int time = rateLimiter.time();
// 获取注解的限流数量
int count = rateLimiter.count();
// 生成键
String combineKey = getCombineKey(rateLimiter, point);
// 将键存入列表
List<Object> keys = Collections.singletonList(combineKey);
try
{
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (StringUtils.isNull(number) || number.intValue() > count)
{
throw new ServiceException("访问过于频繁,请稍候再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
}
catch (ServiceException e)
{
throw e;
}
catch (Exception e)
{
throw new RuntimeException("服务器限流异常,请稍候再试");
}
}
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
// 初始化限流键基础部分
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
// 如果限流类型为 IP,则追加 IP 地址
if (rateLimiter.limitType() == LimitType.IP) {
stringBuffer.append(IpUtils.getIpAddr()).append("-");
}
// 获取方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
// 获取方法对象
Method method = signature.getMethod();
// 获取方法所在类
Class<?> targetClass = method.getDeclaringClass();
// 拼接类名和方法名
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
// 返回组合后的限流键
return stringBuffer.toString();
}
}
@Before("@annotation(rateLimiter)")
将注解@RateLimiter作用切点,在使用该注解的方法前先执行doBefore方法。
getCombineKey方法
该方法主要作用就是生成一个key,具体的注解已经写在代码中了。
获取IP功能在IpUtils类的getIpAddr方法中,感兴趣的话可以自己去看源码。
注:代码来自若依框架,本文章仅讲解。