本文使用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

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐