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、新建切面类拦截限流注解

@Aspect
@Component
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisScript 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 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

你可能感兴趣的:(java实战,spring,boot,lua,java)