SpringBoot+Redis+Lua實(shí)現(xiàn)接口限流的示例代碼
序言
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)文章
詳解SpringBoot中實(shí)現(xiàn)依賴注入功能
這篇文章主要介紹了詳解SpringBoot中實(shí)現(xiàn)依賴注入功能,SpringBoot的實(shí)現(xiàn)方式基本都是通過注解實(shí)現(xiàn)的。有興趣的可以了解一下。2017-04-04redis實(shí)現(xiàn)多進(jìn)程數(shù)據(jù)同步工具代碼分享
這篇文章主要介紹了使用redis實(shí)現(xiàn)多進(jìn)程數(shù)據(jù)同步工具的代碼,大家參考使用吧2014-01-01springboot實(shí)現(xiàn)微信掃碼登錄的項(xiàng)目實(shí)踐
微信掃碼功能是目前第三方登錄常見功能,前不久有個(gè)項(xiàng)目剛好用上,本文主要介紹了springboot實(shí)現(xiàn)微信掃碼登錄的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10Java?項(xiàng)目連接并使用?SFTP?服務(wù)的示例詳解
SFTP是一種安全的文件傳輸協(xié)議,是SSH(Secure?Shell)協(xié)議的一個(gè)子協(xié)議,設(shè)計(jì)用于加密和保護(hù)文件傳輸?shù)陌踩?這篇文章主要介紹了Java?項(xiàng)目如何連接并使用?SFTP?服務(wù)的示例詳解,需要的朋友可以參考下2025-01-01SpringBoot項(xiàng)目實(shí)現(xiàn)短信發(fā)送接口開發(fā)的實(shí)踐
本文主要介紹了SpringBoot項(xiàng)目實(shí)現(xiàn)短信發(fā)送接口開發(fā)的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10GraalVM系列Native?Image?Basics靜態(tài)分析
這篇文章主要為大家介紹了GraalVM系列Native?Image?Basics靜態(tài)分析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02