Springboot+Redis+Lua实现IP限流与过滤

604 阅读3分钟

Springboot+Redis+Lua实现IP限流与过滤

引言

在做项目ip限流这一块时,有思考过用java语言去编写逻辑,后面发现redis自带lua解释器,可以直接执行lua脚本代码,lua本身就有轻便简洁的优点,

使用 lua 脚本的好处:

  • 原子操作:lua脚本是作为一个整体执行的,所以中间不会被其他命令插入。
  • 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 复用性:lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用,也减少了代码量。

注意: lua脚本在redis中是单线程运行的,如果发生错误的话会阻塞线程,所以一定要调试好脚本

需要有一定的lua脚本基础:Lua 数据类型 | 菜鸟教程 (runoob.com)

需要有一定的Redis基础:Redis 脚本 | 菜鸟教程 (runoob.com)

配置

在原有的redis工具类中已经封装了redisTemplate,我们需要新增一个execute执行lua脚本的方法。

/**
     * 在redis中执行一个lua脚本,因为redis配置使用的序列化器是fastjson,所以采用json格式来进行序列化
     *
     * @param script 脚本
     * @param keys   键值集合
     * @param args   参数
     * @return 执行的结果
     */
​
    public RedisEvalRes execute(RedisScript<JSONObject> script, List<String> keys, Object... args) {
        return new RedisEvalRes(
                Objects.requireNonNull(
                        (JSONObject) redisTemplate.execute(script, keys, args)));
    }
package com.lite.common.entity;
​
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
​
/**
 * @author Stranger
 * @version 1.0
 * @description: TODO
 * @date 2022/9/5 19:17
 */
@Data
public class RedisEvalRes {
​
    private static final String RES_FLAG = "result";
    private JSONObject result;
​
    public <T> T getResult(Class<T> tClass){
        return result.getObject(RES_FLAG,tClass);
    }
​
    public RedisEvalRes(JSONObject result) {
        this.result = result;
    }
}
​

需要注意的是,由于项目采用的是fastjson做redis的序列化与反序列化,即采用json字符串做载体,需要进行一定的json处理

开发环境是Windows,Redis在Linux上部署,由于编码以及文件的换行符配置导致Windows下计算的SHA1,与Redis在Linux下缓存的文件SHA1不匹配,导致每次都无法命中缓存,此时可以通过IDEA的文件换行设置,调整脚本文件使用Unix换行符,可以解决不同系统匹配问题。

image.png

简单使用

概念

KEYS ,ARGV 是两个默认的全局变量,

KEYS代表的是键值列表,ARGV代表的是参数列表

cjson是一个由c语言编写的json库

redis是默认的redis操作对象,可以用方法由call() ,pcall(),两者的区别在于,前者发生异常时会将异常抛给调用者,后者则将异常包装成table返回

示例脚本

先执行redis命令

set blog hellowrold
​
local payload = {}
​
local RES_FLAG = "result";
​
local key = KEYS[1]
​
local obj = ARGV[1]
​
local val = redis.call('get',key)
​
payload[RES_FLAG] = val;
​
return cjson.encode(payload)
​
@Test
void scripTest() {
    Resource resource = new ClassPathResource("lua/ipLimit.lua");
​
    RedisEvalRes evalRes = redisCache.execute(
        RedisJsonScript.of(resource),
        Collections.singletonList("blog"),
        1,2);
​
    log.info(evalRes.getResult(String.class));
​
}

输出结果

helloworld

IP限流

lua脚本

local payload = {}
​
local RES_FLAG = "result";
​
--获取传入的键值
local key = KEYS[1]
​
--获取时间限制
local limitTime = tonumber(ARGV[1])
​
--获取次数限制
local maxCount = tonumber(ARGV[2])
​
--让value自增1,如果不存在会自动创建
redis.call('INCRBY', key, 1)
​
--获取当前次数
local currentCount = tonumber(redis.call('GET', key))
​
--大于限制次数则返回-1
if currentCount > maxCount then
    payload[RES_FLAG] = -1
    --等于1说明是第一次访问,则设置key的过期时间
elseif currentCount == 1 then
    payload[RES_FLAG] = currentCount
    redis.call("EXPIRE", key, limitTime)
else
    payload[RES_FLAG] = currentCount
endreturn cjson.encode(payload)
​

java代码

package com.lite.auth.aspectJ;
​
import com.lite.auth.exception.SystemBusyException;
import com.lite.auth.utils.LiteBlogContextUtils;
import com.lite.common.entity.RedisEvalRes;
import com.lite.common.serializer.RedisCache;
import com.lite.common.serializer.RedisJsonScript;
import com.lite.system.annotation.RateLimit;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
​
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
​
/**
 * @author Stranger
 * @version 1.0
 * @description: TODO
 * @date 2022/9/5 20:25
 */
@Aspect
@Component
public class RateLimitAspect {
​
​
    @Autowired
    RedisCache redisCache;
    @Autowired
    LiteBlogContextUtils contextUtils;
​
​
    @Before(value = "@annotation(com.lite.system.annotation.RateLimit)")
    public void rateLimitInterceptor(JoinPoint joinpoint) throws SystemBusyException {
​
        MethodSignature methodSignature = (MethodSignature) joinpoint.getSignature();
​
        Method method = methodSignature.getMethod();
​
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);
​
        //获取当前用户token携带的uuid与方法名拼接成redis key
        StringBuilder keuBuilder = new StringBuilder()
                .append(contextUtils.getLocalUserInfo().getUuid())
                .append(".")
                .append(method.getDeclaringClass())
                .append(".")
                .append(method.getName())
                .append(".")
                .append(Arrays.toString(method.getParameters()))
                .append(".")
                .append(method.getReturnType());
​
        RedisEvalRes evalRes= redisCache.execute(
                RedisJsonScript.of(new ClassPathResource("lua/ipLimit.lua")),
                Collections.singletonList(keuBuilder.toString()),
                rateLimit.limitTime(),rateLimit.maxCount());
​
        Long resultCode = evalRes.getResult(Long.class);
​
        if (resultCode == -1)
            throw new SystemBusyException("系统繁忙,请稍后再试");
    }
}
​

11