springboot+redis+lua实现限流防刷注解
·
本文使用redis+lua脚本实现高并发和高性能限流,lua脚本的好处是:
减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.
1、创建注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 指定timeout()时间内的api请求次数
*/
long max() default 20;
/**
* api请求次数的指定时间(秒),即redis数据过期时间
*/
long timeout() default 60;
}
2、新建切面类拦截限流注解
import com.xxx.api.service.common.exception.BizException;
import com.xxx.api.web.annotation.RateLimit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisScript<Long> rateLimitRedisScript;
@Pointcut("@annotation(com.xy.api.web.annotation.RateLimit)")
public void rateLimit(){}
@Around("rateLimit()")
public Object ponitCut(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
RateLimit rateLimit = AnnotationUtils.findAnnotation(method, RateLimit.class);
if(rateLimit != null){
String redisKey = method.getDeclaringClass().getName() + "." + method.getName();
// CacheConstants为常量类,主要用来定义redis的key前缀名
// RequestUtil.getIp()为获取ip的方法,可根据需求自定义
redisKey += CacheConstants.RATE_LIMIT + CacheConstants.CACHE_SEPARATOR + RequestUtil.getIp();
long max = rateLimit.max(); // api最大请求次数
long timeout = rateLimit.timeout(); // api请求次数的指定时间(秒)
long ttl = TimeUnit.SECONDS.toMillis(timeout); // 转换成毫秒
long nowTime = Instant.now().toEpochMilli(); // 当前时间毫秒
long expire = nowTime - ttl; // 计算过期时间
// 执行lua脚本,返回已请求api的次数,0表示超出请求次数
long execCount = stringRedisTemplate.execute(rateLimitRedisScript, Collections.singletonList(redisKey), nowTime + "", ttl + "", expire + "", max + "");
if(execCount == 0){
throw new BizException("手速太快了,慢点儿吧");
}
}
return point.proceed();
}
}
3、启动时加载lua脚本
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.scripting.support.ResourceScriptSource;
@Component
public class RedisConfig {
/**
* 加载redis限流脚本
*/
@Bean
public RedisScript<Long> rateLimitRedisScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/RateLimit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}
4、编写RateLimit.lua脚本
-- redis key,下标从1开始
local key = KEYS[1]
-- 获取参数:当前时间
local now = tonumber(ARGV[1])
-- 获取参数:api请求次数的指定时间(秒)
local ttl = tonumber(ARGV[2])
-- 获取参数:redis过期时间
local expire = tonumber(ARGV[3])
-- 获取参数:最大api请求次数
local max = tonumber(ARGV[4])
-- 清楚指定区间内过期的数据
redis.call("zremrangebyscore", key, 0, expire)
-- 获取zset中当前元素的个数
local current = tonumber(redis.call("zcard", key))
local next = current + 1
if next > max then
-- 达到限流大小 返回0
return 0
else
-- 往zset中添加一个值
redis.call("zadd", key, now, now)
-- 每次访问均重新设置zset的过期时间(毫秒)
redis.call("pexpire", key, ttl)
return next
end
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)