redis限流器的设计
1.定义注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* redis缓存的注解
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(RateLimits.class)
public @interface RedisRateLimitAttribute {
/**
* {@link #key()}的别名
*
* @return key()的别名
*/
String value() default ""; /**
* key, 支持SpEL表达式解析
*
* @return 限流的key值
*/
String key() default ""; /**
* 限流的优先级
*
* @return 限流器的优先级
*/
int order() default 0; /**
* 执行计数的条件表达式,支持SpEL表达式,如果结果为真,则执行计数
*
* @return 执行计数的条件表达式
*/
String incrCondition() default "true"; /**
* 限流的最大值,支持配置引用
*
* @return 限流的最大值
*/
String limit() default "1"; /**
* 限流的时间范围值,支持配置引用
*
* @return 限流的时间范围值
*/
String intervalInMilliseconds() default "1000"; /**
* 降级的方法名,降级方法的参数与原方法一致或多了一个原方法的ReturnValue的类型
*
* @return 降级的方法名
*/
String fallbackMethod() default "";
}
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* 多重限流注解的存储器
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimits { /**
*
* @return 注解列表
*/
RedisRateLimitAttribute[] value() default {};
}
2. 切面方法
import com.google.common.base.Strings;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
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.EnableAspectJAutoProxy;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import redis.clients.jedis.JedisCluster; //开启AspectJ 自动代理模式,如果不填proxyTargetClass=true,默认为false,
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Component
@Order(-1)
@Aspect
public class RedisRateLimitAspect {
/**
* 日志
*/
private static Logger logger = LoggerFactory.getLogger(RedisRateLimitAspect.class); /**
* SPEL表达式解析器
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); /**
* 获取方法参数名称发现器
*/
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); /**
* Redis集群
*/
@Autowired
private JedisCluster jedisCluster; /**
* springboot自动加载配置信息
*/
@Autowired
private Environment environment; /**
* 切面切入点
*/
@Pointcut("@annotation(com.g2.order.server.annotation.RedisRateLimitAttribute)")
public void rateLimit() { } /**
* 环绕切面
*/
@Around("rateLimit()")
public Object handleControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//获取切入点对应的方法.
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = methodSignature.getMethod(); //获取注解列表
List<RedisRateLimitAttribute> redisRateLimitAttributes =
AnnotatedElementUtils.findMergedRepeatableAnnotations(method, RedisRateLimitAttribute.class)
.stream()
.sorted(Comparator.comparing(RedisRateLimitAttribute::order))
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); if (CollectionUtils.isEmpty(redisRateLimitAttributes)) {
return proceedingJoinPoint.proceed();
} // 切入点所在的实例,调用fallback方法时需要
Object target = proceedingJoinPoint.getTarget();
// 方法入参集合,调用fallback方法时需要
Object[] args = proceedingJoinPoint.getArgs();
if (args == null) {
args = new Object[0];
} // 前置检查
for (RedisRateLimitAttribute rateLimit : redisRateLimitAttributes) {
// 获取限流设置的key(可能有配置占位符和spel表达式)
String key = computeExpress(formatKey(rateLimit.key()), proceedingJoinPoint, String.class);
// 获取限流配置的阀值
long limitV = Long.parseLong(formatKey(rateLimit.limit()));
// 获取当前key已记录的值
String currentValue = jedisCluster.get(key);
long currentV = Strings.isNullOrEmpty(currentValue) ? 0 : Long.parseLong(jedisCluster.get(key));
// 当前值如果小于等于阀值,则合法;否则不合法
boolean validated = currentV <= limitV;
// 如果不合法则进入fallback流程
if (!validated) {
// 获取当前限流配置的fallback
Method fallbackMethod = getFallbackMethod(proceedingJoinPoint, rateLimit.fallbackMethod());
// 如果fallback参数数量与切入点参数数量不一样,则压入空的返回值
if (fallbackMethod.getParameterCount() != method.getParameterCount()) {
Object[] args2 = Arrays.copyOf(args, args.length + 1);
args2[args2.length - 1] = null;
return invokeFallbackMethod(fallbackMethod, target, args2);
} return invokeFallbackMethod(fallbackMethod, target, args);
}
} // 前置检查通过后,执行方法体
Object result = proceedingJoinPoint.proceed(); // 后置检查
for (RedisRateLimitAttribute rateLimit : redisRateLimitAttributes) {
// 获取限流设置的key(可能有配置占位符和spel表达式)
String key = computeExpress(formatKey(rateLimit.key()), proceedingJoinPoint, String.class, result);
// 获取限流配置的阀值
long limitV = Long.parseLong(formatKey(rateLimit.limit()));
// 获取限流配置的限流区间
long interval = Long.parseLong(formatKey(rateLimit.intervalInMilliseconds()));
boolean validated = true;
// 计算当前一次执行后是否满足限流条件
boolean incrMatch = match(proceedingJoinPoint, rateLimit, result);
if (incrMatch) {
// 如果不存在key,则设置该key,并且超时时间为限流区间值
// 获取当前key已记录的值
String currentValue = jedisCluster.get(key);
// TODO 这里最好修改成 lua脚本来实现原子性
long currentV = Strings.isNullOrEmpty(currentValue) ? 0 : Long.parseLong(jedisCluster.get(key));
if (currentV == 0) {
jedisCluster.set(key, "1", "nx", "ex", interval);
} else {
jedisCluster.incrBy(key, 1);
}
validated = currentV +1 <= limitV;
} if (!validated) {
// 获取fallback方法
// TODO 这里可以修改为已获取的话Map里,下次不需要再调用getFallbackMethod方法了
Method fallbackMethod = getFallbackMethod(proceedingJoinPoint, rateLimit.fallbackMethod());
Object[] args2 = Arrays.copyOf(args, args.length + 1);
args2[args2.length - 1] = result;
return invokeFallbackMethod(fallbackMethod, target, args2);
}
} return result;
} /**
* 计算spel表达式
*
* @param expression 表达式
* @param context 上下文
* @return String的缓存key
*/
private <T> T computeExpress(String expression, JoinPoint context, Class<T> tClass) {
// 计算表达式(根据参数上下文)
return computeExpress(expression, context, tClass, null);
} /**
* 计算spel表达式
*
* @param expression 表达式
* @param context 上下文
* @return String的缓存key
*/
private <T> T computeExpress(String expression, JoinPoint context, Class<T> tClass, Object returnValue) {
// 将参数名与参数值放入参数上下文
EvaluationContext evaluationContext = buildEvaluationContext(returnValue, context); // 计算表达式(根据参数上下文)
return EXPRESSION_PARSER.parseExpression(expression).getValue(evaluationContext, tClass);
} /**
* 计算是否匹配限流策略
* @param context
* @param rateLimit
* @param returnValue
* @return
*/
private boolean match(JoinPoint context, RedisRateLimitAttribute rateLimit, Object returnValue) {
return computeExpress(rateLimit.incrCondition(), context, Boolean.class, returnValue);
} /**
* 格式化key
* @param v
* @return
*/
private String formatKey(String v) {
String result = v;
if (Strings.isNullOrEmpty(result)) {
throw new IllegalStateException("key配置不能为空");
}
return environment.resolvePlaceholders(result);
} /**
* 放入参数值到StandardEvaluationContext
*/
private static void addParameterVariable(StandardEvaluationContext evaluationContext, JoinPoint context) {
MethodSignature methodSignature = (MethodSignature) context.getSignature();
Method method = methodSignature.getMethod();
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
if (parameterNames != null && parameterNames.length > 0) {
Object[] args = context.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
}
} /**
* 放入返回值到StandardEvaluationContext
*/
private static void addReturnValue(StandardEvaluationContext evaluationContext, Object returnValue) {
evaluationContext.setVariable("returnValue", returnValue);
evaluationContext.setVariable("response", returnValue);
} /**
* 构建StandardEvaluationContext
*/
private static EvaluationContext buildEvaluationContext(Object returnValue, JoinPoint context) {
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
addParameterVariable(evaluationContext, context);
addReturnValue(evaluationContext, returnValue); return evaluationContext;
} /**
* 获取降级方法
*
* @param context 过滤器上下文
* @param fallbackMethod 失败要执行的函数
* @return 降级方法
*/
private static Method getFallbackMethod(JoinPoint context, String fallbackMethod) {
MethodSignature methodSignature = (MethodSignature) context.getSignature();
Class[] parameterTypes = Optional.ofNullable(methodSignature.getParameterTypes()).orElse(new Class[0]);
try {
Method method = context.getTarget().getClass().getDeclaredMethod(fallbackMethod, parameterTypes);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) { } try {
Class[] parameterTypes2 = Arrays.copyOf(parameterTypes, parameterTypes.length + 1);
parameterTypes2[parameterTypes2.length - 1] = methodSignature.getReturnType(); Method method = context.getTarget().getClass().getDeclaredMethod(fallbackMethod, parameterTypes2);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) { } String message = String.format("获取fallbackMethod失败, context: %s, fallbackMethod: %s",
context, fallbackMethod);
throw new RuntimeException(message);
} /**
* 执行降级fallback方法
* @param fallbackMethod
* @param fallbackTarget
* @param fallbackArgs
* @return
* @throws Throwable
*/
private static Object invokeFallbackMethod(Method fallbackMethod, Object fallbackTarget, Object[] fallbackArgs)
throws Throwable {
try {
return fallbackMethod.invoke(fallbackTarget, fallbackArgs);
} catch (InvocationTargetException e) {
if (e.getCause() != null) {
throw e.getCause();
}
throw e;
}
}
}
3.调用事例
@Slf4j
@Api(value = "HomeController", description = "用户登录登出接口")
@RestController
@RequestMapping("/home")
public class HomeController {
private static Logger logger = LoggerFactory.getLogger(HomeController.class); @ApiOperation(value = "用户登录", notes = "用户登录接口")
@RequestMapping(value = "/login",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody @RedisRateLimitAttribute(key = "'login'+#req.userId"
, limit = "${login.maxFailedTimes:3}"
, incrCondition = "#response.success == true"
, intervalInMilliseconds = "${login.limit.millseconds:3600}"
, fallbackMethod = "loginFallback"
)
public UserLoginResp login(@RequestBody UserLoginReq req) {
logger.info("进入登陆业务"); UserModel userModel = new UserModel();
userModel.setRoleId(123);
userModel.setUserId(req.getUserId());
userModel.setMustValidateCode(false); return new UserLoginResp(userModel);
} private UserLoginResp loginFallback(UserLoginReq req, UserLoginResp resp) {
if (resp == null) {
return new UserLoginResp(); }
resp.getPayload().setMustValidateCode(true);
return resp;
}
}
@Data
public class UserModel {
/***
* 用户id
*/
private String userId; /**
* 角色
*/
private String roleName; /**
* 角色编号
*/
private Integer roleId; /**
* 登陆是否需要验证码
* 当错误次数达到阀值时,需要验证码来增加提交难度
*/
private Boolean mustValidateCode;
}
import lombok.Data; @Data
public class Response<T> {
private Boolean success;
private String errorMessage;
private T payload; public Response() {
this(true);
} public Response(boolean succ) {
this(succ, "");
} public Response(boolean succ, String msg) {
this(succ, msg, null);
} public Response(T data) {
this(true, "", data);
} public Response(boolean succ, String msg, T data) {
success = succ;
errorMessage = msg;
this.payload = data;
}
}
public class UserLoginResp extends Response<UserModel> {
public UserLoginResp(){
}
public UserLoginResp(UserModel userModel){
super(userModel);
}
@Override
public String toString() {
return super.toString();
}
}
redis限流器的设计的更多相关文章
- Redis缓存的设计、性能、应用与数据集群同步
Redis缓存的设计.性能.应用与数据集群同步 http://youzhixueyuan.com/design-performance-and-application-of-redis-cache.h ...
- Redis集群设计原理
---恢复内容开始--- Redis集群设计包括2部分:哈希Slot和节点主从,本篇博文通过3张图来搞明白Redis的集群设计. 节点主从: 主从设计不算什么新鲜玩意,在数据库中我们也经常用主从来做读 ...
- 【集群】Redis集群设计原理
Redis集群设计包括2部分:哈希Slot和节点主从 节点主从: 主从设计不算什么新鲜玩意,在数据库中我们也经常用主从来做读写分离,直接上图: 图上能看得到的信息: 1, 只有1个Master,可以有 ...
- 三张图秒懂Redis集群设计原理
转载Redis Cluster原理 转载https://blog.csdn.net/yejingtao703/article/details/78484151 redis集群部署方式: 单机 主从 r ...
- Redis初识、设计思想与一些学习资源推荐
一.Redis简介 1.什么是Redis Redis 是一个开源的使用ANSI C 语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value 数据库,并提供多种语言的API.从2010 年 ...
- 基于redis的排行榜设计和实现
前言: 最近想实现一个网页闯关游戏的排行榜设计, 相对而言需求比较简单. 秉承前厂长的训导: “做一件事之前, 先看看别人是怎么做的”. 于是乎网上搜索并参考了不少排行榜的实现机制, 很多人都推荐了r ...
- Redis键值设计(转载)
参考资料:https://blog.csdn.net/iloveyin/article/details/7105181 丰富的数据结构使得redis的设计非常的有趣.不像关系型数据库那样,DEV和DB ...
- Redis缓存策略设计及常见问题
Redis缓存设计及常见问题 缓存能够有效地加速应用的读写速度,同时也可以降低后端负载,对日常应用的开发至关重要.下面会介绍缓存使用技巧和设计方案,包含如下内容:缓存的收益和成本分析.缓存更新策略的选 ...
- Redis的持久化设计
Redis 持久化设计 持久化的功能:Redis是内存数据库,数据都是存储在内存中的,为了避免进程退出导致数据的永久丢失,要定期将Redis中的数据以某种形式从内存保存到硬盘,当下次Reids重启时, ...
随机推荐
- 【串线篇】spring boot全面接管springMvc
一.Spring MVC auto-configuration Spring Boot 自动配置好了SpringMVC 以下是SpringBoot对SpringMVC的默认配置:(WebMvcAutoC ...
- 在父组件中,直接获取子组件数据-vue
1.通过 $ref 获取 主父组件中: <x-test ref="ch"></x-test> import XTest from '@/components ...
- HDU 5386 Cover
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5386 题目大意:给一个初始矩阵(n×n).一个目标矩阵(n×n)和m个操作,要求找到一种操作顺序,使初 ...
- LeetCode--055--跳跃游戏(java)
给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1,1,4] 输出: true ...
- darknet-yolov3使用opencv3.4.8时,undefined reference 'imshow()'、'waitKey()'、'nameWindows()'
解决办法:暴力卸载 卸载办法:进入到opencv3.4.8的安装目录下:make uninstall 然后重新安装了其他版本的,立马编译通过了.
- 项目部署到tomcat,浏览器能够访问,手机不能访问。
问题:有这样一个问题,把项目部署到tomcat上,浏览器能够访问,但是手机不能访问. 解决:在 tomcat中找到conf文件夹,然后找到web.xml
- Python 爬虫实战(1):分析豆瓣中最新电影的影评
目标总览 主要做了三件事: 抓取网页数据 清理数据 用词云进行展示 使用的python版本是3.6 一.抓取网页数据 第一步要对网页进行访问,python中使用的是urllib库.代码如下: from ...
- Flask 重定向到动态url
url_for() 函数是动态构建一个网址给特定的功能是非常有用的.该函数接受函数的名称作为第一个参数,并接受一个或多个关键字参数,每个参数对应于URL的变量部分. 以下脚本演示了使用 url_for ...
- upc组队赛18 THE WORLD【时间模拟】
THE WORLD 题目链接 题目描述 The World can indicate world travel, particularly on a large scale. You mau be l ...
- Naive Bayes Algorithm And Laplace Smoothing
朴素贝叶斯算法(Naive Bayes)适用于在Training Set中,输入X和输出Y都是离散型的情况.如果输入X为连续,输出Y为离散,我们考虑使用逻辑回归(Logistic Regression ...