使用AOP+redis+lua做方法限流的實現(xiàn)
需求
公司里使用OneByOne的方式刪除數(shù)據(jù),為了防止一段時間內(nèi)刪除數(shù)據(jù)過多,讓我這邊做一個接口限流,超過一定閾值后報異常,終止刪除操作。
實現(xiàn)方式
創(chuàng)建自定義注解
@limit讓使用者在需要的地方配置count(一定時間內(nèi)最多訪問次數(shù))、period(給定的時間范圍),也就是訪問頻率。然后通過LimitInterceptor攔截方法的請求, 通過 redis+lua 腳本的方式,控制訪問頻率。
源碼
Limit 注解
用于配置方法的訪問頻率count、period
import javax.validation.constraints.Min;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
/**
* key
*/
String key() default "";
/**
* Key的前綴
*/
String prefix() default "";
/**
* 一定時間內(nèi)最多訪問次數(shù)
*/
@Min(1)
int count();
/**
* 給定的時間范圍 單位(秒)
*/
@Min(1)
int period();
/**
* 限流的類型(用戶自定義key或者請求ip)
*/
LimitType limitType() default LimitType.CUSTOMER;
}LimitKey
用于標記參數(shù),作為redis key值的一部分
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitKey {
}LimitType
枚舉,redis key值的類型,支持自定義key和ip、methodName中獲取key
public enum LimitType {
/**
* 自定義key
*/
CUSTOMER,
/**
* 請求者IP
*/
IP,
/**
* 方法名稱
*/
METHOD_NAME;
}RedisLimiterHelper
初始化一個限流用到的redisTemplate Bean
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
@Configuration
public class RedisLimiterHelper {
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(@Qualifier("defaultStringRedisTemplate") StringRedisTemplate redisTemplate) {
RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisTemplate.getConnectionFactory());
return template;
}
}LimitInterceptor
使用 aop 的方式來攔截請求,控制訪問頻率
import com.google.common.collect.ImmutableList;
import com.yxt.qida.api.bean.service.xxv2.openapi.anno.Limit;
import com.yxt.qida.api.bean.service.xxv2.openapi.anno.LimitKey;
import com.yxt.qida.api.bean.service.xxv2.openapi.anno.LimitType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Configuration
public class LimitInterceptor {
private static final String UNKNOWN = "unknown";
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Autowired
public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Around("execution(public * *(..)) && @annotation(com.yxt.qida.api.bean.service.xxv2.openapi.anno.Limit)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
LimitType limitType = limitAnnotation.limitType();
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
/**
* 根據(jù)限流類型獲取不同的key ,如果不傳我們會以方法名作為key
*/
String key;
switch (limitType) {
case IP:
key = getIpAddress();
break;
case CUSTOMER:
key = limitAnnotation.key();
break;
case METHOD_NAME:
String methodName = method.getName();
key = StringUtils.upperCase(methodName);
break;
default:
throw new RuntimeException("limitInterceptor - 無效的枚舉值");
}
/**
* 獲取注解標注的 key,這個是優(yōu)先級最高的,會覆蓋前面的 key 值
*/
Object[] args = pjp.getArgs();
Annotation[][] paramAnnoAry = method.getParameterAnnotations();
for (Annotation[] item : paramAnnoAry) {
int paramIndex = ArrayUtils.indexOf(paramAnnoAry, item);
for (Annotation anno : item) {
if (anno instanceof LimitKey) {
Object arg = args[paramIndex];
if (arg instanceof String && StringUtils.isNotBlank((String) arg)) {
key = (String) arg;
break;
}
}
}
}
if (StringUtils.isBlank(key)) {
throw new RuntimeException("limitInterceptor - key值不能為空");
}
String prefix = limitAnnotation.prefix();
String[] keyAry = StringUtils.isBlank(prefix) ? new String[]{"limit", key} : new String[]{"limit", prefix, key};
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(keyAry, "-"));
try {
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<Number>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
if (count != null && count.intValue() <= limitCount) {
return pjp.proceed();
} else {
String classPath = method.getDeclaringClass().getName() + "." + method.getName();
throw new RuntimeException("limitInterceptor - 限流被觸發(fā):"
+ "class:" + classPath
+ ", keys:" + keys
+ ", limitcount:" + limitCount
+ ", limitPeriod:" + limitPeriod + "s");
}
} catch (Throwable e) {
if (e instanceof RuntimeException) {
throw new RuntimeException(e.getLocalizedMessage());
}
throw new RuntimeException("limitInterceptor - 限流服務異常");
}
}
/**
* lua 腳本,為了保證執(zhí)行 redis 命令的原子性
*/
public String buildLuaScript() {
StringBuilder lua = new StringBuilder();
lua.append("local c");
lua.append("\nc = redis.call('get',KEYS[1])");
// 調用不超過最大值,則直接返回
lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
lua.append("\nreturn c;");
lua.append("\nend");
// 執(zhí)行計算器自加
lua.append("\nc = redis.call('incr',KEYS[1])");
lua.append("\nif tonumber(c) == 1 then");
// 從第一次調用開始限流,設置對應鍵值的過期
lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
lua.append("\nend");
lua.append("\nreturn c;");
return lua.toString();
}
public String getIpAddress() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}TestService
使用方式示例
@Limit(period = 10, count = 10)
public String delUserByUrlTest(@LimitKey String token, String thirdId, String url) throws IOException {
return "success";
}到此這篇關于使用AOP+redis+lua做方法限流的實現(xiàn)的文章就介紹到這了,更多相關AOP+redis+lua限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redisson實現(xiàn)分布式鎖、鎖續(xù)約的案例
這篇文章主要介紹了Redisson如何實現(xiàn)分布式鎖、鎖續(xù)約,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03
Redis結合 Docker 搭建集群并整合SpringBoot的詳細過程
這篇文章主要介紹了Redis結合Docker搭建集群并整合SpringBoot的詳細過程,本文給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-06-06
Govern Service 基于 Redis 的服務治理平臺安裝過程詳解
Govern Service 是一個輕量級、低成本的服務注冊、服務發(fā)現(xiàn)、 配置服務 SDK,通過使用現(xiàn)有基礎設施中的 Redis 不用給運維部署帶來額外的成本與負擔,接下來通過本文給大家分享Govern Service 基于 Redis 的服務治理平臺的相關知識,感興趣的朋友一起看看吧2021-05-05
Redis報錯NOAUTH?Authentication?required簡單解決辦法
這篇文章主要給大家介紹了關于Redis報錯NOAUTH?Authentication?required的簡單解決辦法,Redis無密碼報錯NOAUTH Authentication required的原因是客戶端訪問Redis時需要提供密碼,但是沒有提供或提供的密碼不正確,需要的朋友可以參考下2024-05-05

