1. 程式人生 > 其它 >Redis+Lua限流元件封裝

Redis+Lua限流元件封裝

技術標籤:AOPredis

1.熟悉下lua語法

2.lua安裝,IDEA安裝lua外掛

安裝Lua:
1. 參考http://www.lua.org/ftp/教程,下載5.3.5_1版本,本地安裝
如果是Mac,那用brew工具直接執行brew install lua就可以順利安裝,
有關brew工具的安裝可以參考https://brew.sh/網站,建議翻牆否則會很慢。

使用brew安裝後的目錄在/usr/local/Cellar/lua/5.3.5_1

2. 安裝IDEA外掛,在IDEA->Preferences面板,Plugins,
裡面Browse repositories,在裡面搜尋lua,然後就選擇同名外掛lua。安裝好後重啟IDEA

3. 配置Lua SDK的位置: IDEA->File->Project Structure,
選擇新增Lua,路徑指向Lua SDK的bin資料夾,我本地的地址是:
/usr/local/Cellar/lua/5.3.5_1/bin

3.建立jar模組,作為公用元件

具體的檔案及程式碼內容如下:

1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>xxx</artifactId>
        <groupId>com.xxx</groupId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>ratelimiter-annotation</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>

    </dependencies>

</project>

2.resource目錄下新增lua指令碼

ratelimiter.lua

-- 獲取方法簽名特徵
local methodKey = KEYS[1]
redis.log(redis.LOG_DEBUG, 'key is', methodKey)

-- 呼叫指令碼傳入的限流大小
local limit = tonumber(ARGV[1])

-- 獲取當前流量大小
local count = tonumber(redis.call('get', methodKey) or "0")

-- 是否超出限流閾值
if count + 1 > limit then
    -- 拒絕服務訪問
    return false
else
    -- 沒有超過閾值
    -- 設定當前訪問的數量+1
    redis.call("INCRBY", methodKey, 1)
    -- 設定過期時間:秒
    redis.call("EXPIRE", methodKey, 1)
    -- 放行
    return true
end

3.src目錄包結構自己定義,新增配置類

RedisConfiguration.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

@Configuration
public class RedisConfiguration {

    // 如果本地也配置了StringRedisTemplate,可能會產生衝突
    // 可以指定@Primary,或者指定載入特定的@Qualifier
    @Bean
    public RedisTemplate<String, String> redisTemplate(
            RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }

    @Bean
    public DefaultRedisScript loadRedisScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setLocation(new ClassPathResource("ratelimiter.lua"));
        redisScript.setResultType(java.lang.Boolean.class);
        return redisScript;
    }

}

4.定義註解截切面類與具體通過redis呼叫lua指令碼實現限流的兩個類。放到建立的一個annotation包下

AccessLimiter.java

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {

    int limit();

    String methodKey() default "";

}

AccessLimiterAspect.java

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;

@Slf4j
@Aspect
@Component
public class AccessLimiterAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisScript<Boolean> rateLimitLua;

    @Pointcut("@annotation(com.xxx.xxx.annotation.AccessLimiter)")
    public void cut() {
        log.info("cut");
    }

    @Before("cut()")
    public void before(JoinPoint joinPoint) {
        // 1. 獲得方法簽名,作為method Key
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
        if (annotation == null) {
            return;
        }

        String key = annotation.methodKey();
        Integer limit = annotation.limit();

        // 如果沒設定methodkey, 從呼叫方法簽名生成自動一個key
        if (StringUtils.isEmpty(key)) {
            Class[] type = method.getParameterTypes();
            key = method.getClass() + method.getName();

            if (type != null) {
                String paramTypes = Arrays.stream(type)
                        .map(Class::getName)
                        .collect(Collectors.joining(","));
                log.info("param types: " + paramTypes);
                key += "#" + paramTypes;
            }
        }

        // 2. 呼叫Redis
        boolean acquired = stringRedisTemplate.execute(
                rateLimitLua, // Lua script的真身
                Lists.newArrayList(key), // Lua指令碼中的Key列表
                limit.toString() // Lua指令碼Value列表
        );

        if (!acquired) {
            log.error("your access is blocked, key={}", key);
            throw new RuntimeException("Your access is blocked");
        }
    }

}

至此redis+lua限流元件開發完成。下面在其他模組中使用測試。

使用步驟:

1.在需要用到的工程中引入限流元件的依賴包

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>ratelimiter-annotation</artifactId>
    <version>${project.version}</version>
</dependency>

2.在需要限流的controller介面上進行限流注解即可。

// limit後的值為限流流量數限1秒1個請求
@GetMapping("test-annotation")
@com.xxx.xxx.annotation.AccessLimiter(limit = 1)
public String testAnnotation() {
    return "success";
}