SpringBoot+Redis+Lua分布式限流的實(shí)現(xiàn)
Redis支持LUA腳本的主要優(yōu)勢(shì)
LUA腳本的融合將使Redis數(shù)據(jù)庫產(chǎn)生更多的使用場(chǎng)景,迸發(fā)更多新的優(yōu)勢(shì):
- 高效性:減少網(wǎng)絡(luò)開銷及時(shí)延,多次redis服務(wù)器網(wǎng)絡(luò)請(qǐng)求的操作,使用LUA腳本可以用一個(gè)請(qǐng)求完成
- 數(shù)據(jù)可靠性:Redis會(huì)將整個(gè)腳本作為一個(gè)整體執(zhí)行,中間不會(huì)被其他命令插入。
- 復(fù)用性:LUA腳本執(zhí)行后會(huì)永久存儲(chǔ)在Redis服務(wù)器端,其他客戶端可以直接復(fù)用
- 可嵌入性:可嵌入JAVA,C#等多種編程語言,支持不同操作系統(tǒng)跨平臺(tái)交互
- 簡單強(qiáng)大:小巧輕便,資源占用率低,支持過程化和對(duì)象化的編程語言
自己也是第一次在工作中使用lua這種語言,記錄一下
創(chuàng)建Lua文件req_ratelimit.lua
local key = KEYS[1] --限流KEY
local limitCount = tonumber(ARGV[1]) --限流大小
local limitTime = tonumber(ARGV[2]) --限流時(shí)間
local current = redis.call('get', key);
if current then
if current + 1 > limitCount then --如果超出限流大小
return 0
else
redis.call("INCRBY", key,"1")
return current + 1
end
else
redis.call("set", key,"1")
redis.call("expire", key,limitTime)
return 1
end
自定義注解RateLimiter
package com.shinedata.ann;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
/**
* 限流唯一標(biāo)識(shí)
* @return
*/
String key() default "rate.limit:";
/**
* 限流時(shí)間
* @return
*/
int time() default 1;
/**
* 限流次數(shù)
* @return
*/
int count() default 100;
/**
*是否限制IP,默認(rèn) 否
* @return
*/
boolean restrictionsIp() default false;
}定義切面RateLimiterAspect
package com.shinedata.aop;
import com.shinedata.ann.RateLimiter;
import com.shinedata.config.redis.RedisUtils;
import com.shinedata.exception.RateLimiterException;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
/**
* @ClassName RateLimiterAspect
* @Author yupanpan
* @Date 2020/5/6 13:46
*/
@Aspect
@Component
public class RateLimiterAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static ThreadLocal<String> ipThreadLocal=new ThreadLocal();
private DefaultRedisScript<Number> redisScript;
@PostConstruct
public void init(){
redisScript = new DefaultRedisScript<Number>();
redisScript.setResultType(Number.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/req_ratelimit.lua")));
}
@Around("@annotation(com.shinedata.ann.RateLimiter)")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
RateLimiter rateLimit = method.getAnnotation(RateLimiter.class);
if (rateLimit != null) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
boolean restrictionsIp = rateLimit.restrictionsIp();
if(restrictionsIp){
ipThreadLocal.set(getIpAddr(request));
}
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(rateLimit.key());
if(StringUtils.isNotBlank(ipThreadLocal.get())){
stringBuffer.append(ipThreadLocal.get()).append("-");
}
stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
List<String> keys = Collections.singletonList(stringBuffer.toString());
Number number = RedisUtils.execute(redisScript, keys, rateLimit.count(), rateLimit.time());
if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
logger.info("限流時(shí)間段內(nèi)訪問第:{} 次", number.toString());
return joinPoint.proceed();
}else {
logger.error("已經(jīng)到設(shè)置限流次數(shù),當(dāng)前次數(shù):{}",number.toString());
throw new RateLimiterException("服務(wù)器繁忙,請(qǐng)稍后再試");
}
} else {
return joinPoint.proceed();
}
}finally {
ipThreadLocal.remove();
}
}
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
// 對(duì)于通過多個(gè)代理的情況,第一個(gè)IP為客戶端真實(shí)IP,多個(gè)IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()= 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}Spring data redis提供了DefaultRedisScript來使用lua和redis進(jìn)行交互,具體的詳情網(wǎng)上很多文章,這里使用ThreadLocal是因?yàn)镮P存在可變的,保證自己的線程的IP不會(huì)被其他線程所修改,切記要最后清理ThreadLocal,防止內(nèi)存泄漏
RedisUtils工具類(方法太多,只展示execute方法)
package com.shinedata.config.redis;
import org.checkerframework.checker.units.qual.K;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RedisUtils
* @Author yupanpan
* @Date 2019/11/20 13:38
*/
@Component
public class RedisUtils {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Object> redisTemplate;
private static RedisUtils redisUtils;
@PostConstruct
public void init() {
redisUtils = this;
redisUtils.redisTemplate = this.redisTemplate;
}
public static Number execute(DefaultRedisScript<Number> script, List keys, Object... args) {
return redisUtils.redisTemplate.execute(script, keys,args);
}
}自己配置的RedisTemplate
package com.shinedata.config.redis;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
/**
* @ClassName RedisConfig
* @Author yupanpan
* @Date 2019/11/20 13:26
*/
@Configuration
public class RedisConfig extends RedisProperties{
protected Logger log = LogManager.getLogger(RedisConfig.class);
/**
* JedisPoolConfig 連接池
* @return
*/
@Bean("jedisPoolConfig")
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空閑數(shù)
jedisPoolConfig.setMaxIdle(500);
jedisPoolConfig.setMinIdle(100);
// 連接池的最大數(shù)據(jù)庫連接數(shù)
jedisPoolConfig.setMaxTotal(6000);
// 最大建立連接等待時(shí)間
jedisPoolConfig.setMaxWaitMillis(5000);
// 逐出連接的最小空閑時(shí)間 默認(rèn)1800000毫秒(30分鐘)
jedisPoolConfig.setMinEvictableIdleTimeMillis(100);
// 每次逐出檢查時(shí) 逐出的最大數(shù)目 如果為負(fù)數(shù)就是 : 1/abs(n), 默認(rèn)3
// jedisPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
// 逐出掃描的時(shí)間間隔(毫秒) 如果為負(fù)數(shù),則不運(yùn)行逐出線程, 默認(rèn)-1
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(600);
// 是否在從池中取出連接前進(jìn)行檢驗(yàn),如果檢驗(yàn)失敗,則從池中去除連接并嘗試取出另一個(gè)
jedisPoolConfig.setTestOnBorrow(true);
// 在空閑時(shí)檢查有效性, 默認(rèn)false
jedisPoolConfig.setTestWhileIdle(false);
return jedisPoolConfig;
}
/**
* JedisConnectionFactory
* @param jedisPoolConfig
*/
@Bean("jedisConnectionFactory")
public JedisConnectionFactory jedisConnectionFactory(@Qualifier("jedisPoolConfig")JedisPoolConfig jedisPoolConfig) {
JedisConnectionFactory JedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig);
// 連接池
JedisConnectionFactory.setPoolConfig(jedisPoolConfig);
// IP地址
JedisConnectionFactory.setHostName(redisHost);
// 端口號(hào)
JedisConnectionFactory.setPort(redisPort);
// 如果Redis設(shè)置有密碼
JedisConnectionFactory.setPassword(redisPassword);
// 客戶端超時(shí)時(shí)間單位是毫秒
JedisConnectionFactory.setTimeout(10000);
return JedisConnectionFactory;
}
/**
* 實(shí)例化 RedisTemplate 對(duì)象代替原有的RedisTemplate<String, String>
* @return
*/
@Bean("redisTemplate")
public RedisTemplate<String, Object> functionDomainRedisTemplate(@Qualifier("jedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* 設(shè)置數(shù)據(jù)存入 redis 的序列化方式
* @param redisTemplate
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
// 如果不配置Serializer,那么存儲(chǔ)的時(shí)候缺省使用String,比如如果用User類型存儲(chǔ),那么會(huì)提示錯(cuò)誤User can't cast
// to String!
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 開啟事務(wù)/true必須手動(dòng)釋放連接,false會(huì)自動(dòng)釋放連接 如果調(diào)用方有用@Transactional做事務(wù)控制,可以開啟事務(wù),Spring會(huì)處理連接問題
redisTemplate.setEnableTransactionSupport(false);
redisTemplate.setConnectionFactory(factory);
}
}全局Controller異常處理GlobalExceptionHandler
package com.shinedata.exception;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.shinedata.util.ResultData;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = RateLimiterException.class)
@ResponseStatus(HttpStatus.OK)
public ResultData runtimeExceptionHandler(RateLimiterException e) {
logger.error("系統(tǒng)錯(cuò)誤:", e);
return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "處理失敗");
}
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.OK)
public ResultData runtimeExceptionHandler(RuntimeException e) {
Throwable cause = e.getCause();
logger.error("系統(tǒng)錯(cuò)誤:", e);
logger.error(e.getMessage());
if (cause instanceof JsonMappingException) {
return ResultData.getResultError("參數(shù)錯(cuò)誤");
}
return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "處理失敗");
}
}使用就很簡單了,一個(gè)注解搞定

補(bǔ)充:優(yōu)化了lua為
local key = KEYS[1]
local limitCount = tonumber(ARGV[1])
local limitTime = tonumber(ARGV[2])
local current = redis.call('get', key);
if current then
redis.call("INCRBY", key,"1")
return current + 1
else
redis.call("set", key,"1")
redis.call("expire", key,limitTime)
return 1
end到此這篇關(guān)于SpringBoot+Redis+Lua分布式限流的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot Redis Lua分布式限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- redis+lua實(shí)現(xiàn)分布式限流的示例
- 詳解Redisson分布式限流的使用及原理
- Redisson分布式限流的實(shí)現(xiàn)原理分析
- Redisson分布式限流器RRateLimiter的使用及原理小結(jié)
- 利用redis lua腳本實(shí)現(xiàn)時(shí)間窗分布式限流
- Redis分布式限流的幾種實(shí)現(xiàn)
- Redisson分布式限流的實(shí)現(xiàn)原理解析
- redisson分布式限流RRateLimiter源碼解析
- Redis分布式限流組件設(shè)計(jì)與使用實(shí)例
- Redis實(shí)現(xiàn)分布式限流的幾種方法
相關(guān)文章
Intellij IDEA 關(guān)閉和開啟自動(dòng)更新的提示?
這篇文章主要介紹了Intellij IDEA 關(guān)閉和開啟自動(dòng)更新的提示操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04
Spring中容器的創(chuàng)建流程詳細(xì)解讀
這篇文章主要介紹了Spring中容器的創(chuàng)建流程詳細(xì)解讀,Spring?框架其本質(zhì)是作為一個(gè)容器,提供給應(yīng)用程序需要的對(duì)象,了解容器的誕生過程,有助于我們理解?Spring?框架,也便于我們“插手”這個(gè)過程,需要的朋友可以參考下2023-10-10
Java ConcurrentHashMap鎖分段機(jī)制使用及代碼實(shí)例
ConcurrentHashMap是Java中的一種線程安全的哈希表,通過鎖分段機(jī)制提高了并發(fā)性能,在Java 8中,ConcurrentHashMap引入了CAS操作和更復(fù)雜的節(jié)點(diǎn)繼承結(jié)構(gòu),進(jìn)一步優(yōu)化了并發(fā)操作2025-01-01
SpringBoot2零基礎(chǔ)到精通之?dāng)?shù)據(jù)庫專項(xiàng)精講
SpringBoot是一種整合Spring技術(shù)棧的方式(或者說是框架),同時(shí)也是簡化Spring的一種快速開發(fā)的腳手架,本篇我們來學(xué)習(xí)如何連接數(shù)據(jù)庫進(jìn)行操作2022-03-03

