欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot+Redis+Lua實(shí)現(xiàn)接口限流的示例代碼

 更新時(shí)間:2025年09月19日 10:37:09   作者:月生_  
本文主要介紹了SpringBoot+Redis+Lua實(shí)現(xiàn)接口限流的示例代碼,利用AOP攔截請(qǐng)求,通過Redis記錄訪問次數(shù),達(dá)到限流效果,具有一定的參考價(jià)值,感興趣的可以了解一下

序言

Lua 是一種輕量小巧的腳本語言,用標(biāo)準(zhǔn)C語言編寫并以源代碼形式開放, 其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。這篇文章圍繞Radis和Lua腳本來實(shí)現(xiàn)接口的限流

1.導(dǎo)入依賴

Lua腳本其在Redis2.6及以上的版本就已經(jīng)內(nèi)置了,所以需要導(dǎo)入的依賴如下:

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

2.配置Redis環(huán)境

依賴導(dǎo)入成功后,需要在項(xiàng)目當(dāng)中配置Redis的環(huán)境,這是程序和Redis交互的重要步驟。 在SpringBoot項(xiàng)目的資源路徑下找到application.yml配置文件,內(nèi)容如下:

spring: 
    redis: host: 127.0.0.1 
    port: 6379 
    database: 0 
    password: 
    timeout: 10s 
    lettuce: 
        pool: 
            min-idle: 0  #連接池中的最小空閑連接數(shù)為 0。這意味著在沒有任何請(qǐng)求時(shí),連接池可以沒有空閑連接。
            max-idle: 8  #連接池中的最大空閑連接數(shù)為 8。當(dāng)連接池中的空閑連接數(shù)超過這個(gè)值時(shí),多余的連接可能會(huì)被關(guān)閉以節(jié)省資源。
            max-active: 8  #連接池允許的最大活動(dòng)連接數(shù)為 8。在并發(fā)請(qǐng)求較高時(shí),連接池最多可以創(chuàng)建 8 個(gè)連接來滿足需求。
            max-wait: -1ms  #當(dāng)連接池中的連接都被使用且沒有空閑連接時(shí),新的連接請(qǐng)求等待獲取連接的最大時(shí)間。這里設(shè)置為 -1ms,表示無限等待,直到有可用連接為止。
           

3.創(chuàng)建限流類型

我們既然需要對(duì)一個(gè)接口進(jìn)行限流,那么就需要配置應(yīng)該以何種規(guī)則進(jìn)行限流,比如ip地址、地理位置限流等,我們這里以ip限流為例。創(chuàng)建限流枚舉類:

public enum LimitType { 
/** * 針對(duì)某一個(gè)ip進(jìn)行限流 */ 
    IP("IP") ; 
    
    private final String type; 
    
    LimitType(String type) { 
        this.type = type; 
    } 
    
    
    public String getType() { 
        return type; 
    } 
}

4.創(chuàng)建限流注解

自定義限流類型完成以后,需要定義限流注解,然后在需要被限流訪問的接口上添加上限流注解,結(jié)合AOP切面即可實(shí)現(xiàn)限流的操作。限流注解定義如下:

import java.lang.annotation.*; 


@Target(ElementType.METHOD) 
@Retention(RetentionPolicy.RUNTIME) 
@Documented 
public @interface RateLimiter { 
    /** * 限流類型 * @return */ 
    LimitType limitType() default LimitType.IP; 
    
    /** * 限流key * @return */ 
    String key() default ""; 
    
    /** * 限流時(shí)間 * @return */ 
    int time() default 60; 
    
    /** * 限流次數(shù) * @return */ 
    int count() default 100; 
}

5.編寫限流的Lua腳本

我這里是創(chuàng)建了一個(gè).lua結(jié)尾的文件,并把文章放在了項(xiàng)目資源的根路徑下,也可以不創(chuàng)建文件,而是使用文本字符串的方式來編寫腳本內(nèi)容(稍后說明)。Lua腳本內(nèi)容如下:

local key = KEYS[1] 
local time = tonumber(ARGV[1]) 
local count = tonumber(ARGV[2]) 

local current = redis.call('get', key) 

if current and tonumber(current) > count then 
    return tonumber(current) 
end 

current = redis.call('incr', key) 

if tonumber(current) == 1 then 
    redis.call('expire', key, time)
end 

return tonumber(current) 

說明:redis.call('incr', key)命令可以使在Lua腳本調(diào)用Redis中的命令,該行代碼的意思是使緩存中Key所對(duì)應(yīng)的value值自增,如果Redis中Key所對(duì)應(yīng)的值超過了count (限流次數(shù)),則直接返回count數(shù)量,如果沒有超過count數(shù)量,則使value值+1

6.配置RedisConfig

接下來,需要配置RedisConfig的內(nèi)容,比如以哪種序列化方式來序列化Key和Value,以及腳本執(zhí)行器。代碼如下:

import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.core.annotation.Order; 
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.script.DefaultRedisScript; 
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 
import org.springframework.data.redis.serializer.StringRedisSerializer; 
import org.springframework.scripting.support.ResourceScriptSource; 
import java.nio.charset.Charset; 
import java.nio.charset.StandardCharsets; 


@Configuration public class RedisConfig { 

    /** * RedisTemplate配置 * * @param factory * @return */ 
    @Bean 
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {  
            RedisTemplate<String, Object> template = new RedisTemplate<>(); 
            template.setConnectionFactory(factory); 
            StringRedisSerializer serializer = new StringRedisSerializer(StandardCharsets.UTF_8); 
            
            // 使用StringRedisSerializer來序列化和反序列化redis的key值 
            template.setKeySerializer(serializer); 
            template.setValueSerializer(serializer); 
            
            template.setHashKeySerializer(serializer); 
            template.setHashValueSerializer(serializer); 
            
            template.afterPropertiesSet(); 
            
            return template; 
    } 
    
    
    
    /** * Redis Lua 腳本 * * @return */ 
    @Bean DefaultRedisScript<Long> limitScript() { 
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(); 
        script.setResultType(Long.class); 
        // 我這里是以資源文件的形式來加載的lua腳本
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua"))); 
        
        return script; 
       
    } 
}

也可以使用字符串文本的形式來加載

@Bean 
DefaultRedisScript<Long> limitScript() { 
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(); 
        script.setResultType(Long.class);
        
         // 也可以使用文本字符串的形式來加載Lua腳本
        script.setScriptText("local key = KEYS[1]\n" +
                "local time = tonumber(ARGV[1])\n" +
                "local count = tonumber(ARGV[2])\n" +
                "local current = redis.call('get', key)\n" +
                "\n" +
                "if current and tonumber(current) > count then\n" +
                "    return tonumber(current)\n" +
                "end\n" +
                "\n" +
                "current = redis.call('incr', key)\n" +
                "\n" +
                "if tonumber(current) == 1 then\n" +
                "    redis.call('expire', key, time)\n" +
                "end\n" +
                "\n" +
                "return tonumber(current)\n" +
                "\n");
        return script;
}

7.編寫限流切面 RateLimitAspect

前面的步驟完成之后,到了最后一步,編寫限流的注解的AOP切換,在切面中通過Redis調(diào)用Lua腳本來判斷當(dāng)前請(qǐng)求是否達(dá)到限流的條件,如果達(dá)到則為拋出錯(cuò)誤,由全局異常捕獲返回給前端

import com.example.luatest.annotition.RateLimiter; 
import com.example.luatest.exception.IPException; 
import lombok.extern.java.Log; 
import org.aspectj.lang.JoinPoint; 
import org.aspectj.lang.ProceedingJoinPoint; 
import org.aspectj.lang.annotation.Aspect; 
import org.aspectj.lang.annotation.Before; 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
import org.springframework.context.annotation.Lazy; 
import org.springframework.data.redis.core.RedisTemplate; 
import org.springframework.data.redis.core.StringRedisTemplate; 
import org.springframework.data.redis.core.script.DefaultRedisScript; 
import org.springframework.stereotype.Component; 
import javax.annotation.Resource; 
import javax.servlet.http.HttpServletRequest; 
import java.util.Collections; 
import java.util.List; 

@Aspect 
@Component //切面類也需要加入到ioc容器 
public class RateLimitAspect { 

    private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitAspect.class); 
    
    private final RedisTemplate<String, Object> redisTemplate; 
    
    private final DefaultRedisScript<Long> limitScript; 
    
    public RateLimitAspect(RedisTemplate<String, Object> redisTemplate, DefaultRedisScript<Long> limitScript) { 
        this.redisTemplate = redisTemplate; 
        this.limitScript = limitScript; 
    } 
    
    @Before("@annotation(rateLimiter)") 
    public void isAllowed(JoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws IPException, InstantiationException, IllegalAccessException { 
        String ip = null; 
        Object[] args = proceedingJoinPoint.getArgs(); 
        for (Object arg : args) { 
            if (arg instanceof HttpServletRequest) { 
                HttpServletRequest request = (HttpServletRequest) arg; 
                ip = request.getRemoteHost(); break;
            } 
        } 
    
        LOGGER.info("ip:{}", ip); 
        if (ip == null) { 
            throw new IPException("ip is null"); 
        } 
    
        //拼接redis建 
        String key = rateLimiter.key() + ip; 

        // 執(zhí)行 Lua 腳本進(jìn)行限流判斷
        List<String> keyList = Collections.singletonList(key); 
    
        Long result = redisTemplate.execute(limitScript, keyList, key, Integer.toString(rateLimiter.count()), Integer.toString(rateLimiter.time())); 

        LOGGER.info("result:{}", result); 

        if (result != null && result > rateLimiter.count()) { 
            throw new IPException("IP [" + ip + "] 訪問過于頻繁,已超出限流次數(shù)"); 
        } 
    }
}

8.使用注解

最后在方法上使用該限流注解即可

import com.example.luatest.annotition.RateLimiter; 
import com.example.luatest.enum_.LimitType; 
import org.springframework.web.bind.annotation.GetMapping; 
import org.springframework.web.bind.annotation.RequestMapping; 
import org.springframework.web.bind.annotation.RestController; 
import javax.servlet.http.HttpServletRequest; 

@RestController 
@RequestMapping("/rate") 
public class RateController { 

    @RateLimiter(count = 100, time = 60, limitType = LimitType.IP) @GetMapping("/someMethod") 
    public void someMethod(HttpServletRequest request) { 
        // 方法的具體邏輯 
    } 
}

總結(jié):

這篇文章到這里就結(jié)束,總的來說具體思路比較簡單,我們通過創(chuàng)建限流注解,定義限流次數(shù)和間隔時(shí)間,然后對(duì)該注解進(jìn)行AOP切面,在切面當(dāng)中調(diào)用Lua腳本來判斷是否達(dá)到限流條件,如果達(dá)到就拋出錯(cuò)誤,由全局異常捕獲,沒有則代碼繼續(xù)執(zhí)行。

到此這篇關(guān)于SprinBoot + Redis +Lua 實(shí)現(xiàn)接口限流的示例代碼的文章就介紹到這了,更多相關(guān)SprinBoot  Redis Lua接口限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論