SpringBoot+Redis+Lua實現(xiàn)接口限流的示例代碼
序言
Lua 是一種輕量小巧的腳本語言,用標(biāo)準(zhǔn)C語言編寫并以源代碼形式開放, 其設(shè)計目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。這篇文章圍繞Radis和Lua腳本來實現(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)入成功后,需要在項目當(dāng)中配置Redis的環(huán)境,這是程序和Redis交互的重要步驟。 在SpringBoot項目的資源路徑下找到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。這意味著在沒有任何請求時,連接池可以沒有空閑連接。
max-idle: 8 #連接池中的最大空閑連接數(shù)為 8。當(dāng)連接池中的空閑連接數(shù)超過這個值時,多余的連接可能會被關(guān)閉以節(jié)省資源。
max-active: 8 #連接池允許的最大活動連接數(shù)為 8。在并發(fā)請求較高時,連接池最多可以創(chuàng)建 8 個連接來滿足需求。
max-wait: -1ms #當(dāng)連接池中的連接都被使用且沒有空閑連接時,新的連接請求等待獲取連接的最大時間。這里設(shè)置為 -1ms,表示無限等待,直到有可用連接為止。
3.創(chuàng)建限流類型
我們既然需要對一個接口進(jìn)行限流,那么就需要配置應(yīng)該以何種規(guī)則進(jìn)行限流,比如ip地址、地理位置限流等,我們這里以ip限流為例。創(chuàng)建限流枚舉類:
public enum LimitType {
/** * 針對某一個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切面即可實現(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 "";
/** * 限流時間 * @return */
int time() default 60;
/** * 限流次數(shù) * @return */
int count() default 100;
}
5.編寫限流的Lua腳本
我這里是創(chuàng)建了一個.lua結(jié)尾的文件,并把文章放在了項目資源的根路徑下,也可以不創(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所對應(yīng)的value值自增,如果Redis中Key所對應(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)前請求是否達(dá)到限流的條件,如果達(dá)到則為拋出錯誤,由全局異常捕獲返回給前端
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ù)和間隔時間,然后對該注解進(jìn)行AOP切面,在切面當(dāng)中調(diào)用Lua腳本來判斷是否達(dá)到限流條件,如果達(dá)到就拋出錯誤,由全局異常捕獲,沒有則代碼繼續(xù)執(zhí)行。
到此這篇關(guān)于SprinBoot + Redis +Lua 實現(xiàn)接口限流的示例代碼的文章就介紹到這了,更多相關(guān)SprinBoot Redis Lua接口限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis實現(xiàn)多進(jìn)程數(shù)據(jù)同步工具代碼分享
這篇文章主要介紹了使用redis實現(xiàn)多進(jìn)程數(shù)據(jù)同步工具的代碼,大家參考使用吧2014-01-01
GraalVM系列Native?Image?Basics靜態(tài)分析
這篇文章主要為大家介紹了GraalVM系列Native?Image?Basics靜態(tài)分析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02

