本文使用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、新建切面类拦截限流注解
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisScript<Long> rateLimitRedisScript;
@Pointcut("@annotation(com.demo.test.model.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的方法,可根据需求自定义
key += CacheConstants.RATE_LIMIT + CacheConstants.CACHE_SEPARATOR + RequestUtil.getIp();
Long max = rateLimit.max(); // api最大请求次数
Long timeout = rateLimit.timeout(); // api请求次数的指定时间(秒)
Long ttl = TimeUnit.SECONDS.toMillis(); // 转换成毫秒
Long nowTime = Instant.now.toEpochMilli(); // 当前时间毫秒
Long expire = nowTime - ttl; // 计算过期时间
// 执行lua脚本,返回已请求api的次数,0表示超出请求次数
Long exexCount = stringRedisTemplate.excute(rateLimitRedisScript, Collections.singletonList(redisKey), nowTime + "", ttl + "", expire + "", max + "");
if(exexCount != null && exexCount ==0){
throw new BizException("手速太快了,慢点儿吧");
}
}
return point.proceed();
}
}3、启动时加载lua脚本
@Component
public class MoaRedisConfig {
/**
* 加载redis限流脚本
*/
@Bean
public RedisScript<Long> rateLimitRedisScript(){
DefaultRedisScript redisScript = new DefaultRedisScript();
redisScript.setScriptSource(new ResourceScriptResource(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(ARRGV[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版权声明:本文为yujiubo2008原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。