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重启时, ...
随机推荐
- 理解Promise (1)
new Promise 需要传递一个执行器 (函数) 函数有两个参数 resolve reject promise 承诺 默认的状态是pengding 调用 resolve 表示成功 reject 表 ...
- UML建模重点圈划
面向对象的特征 *P9*>封装性>继承性>多态性>传递性 建模语言的三个类别 *P14*> - 非形式化的.半形式化的和形式化的 UML 特点*15*主要有三个特点:&g ...
- C# 常用的ToString("xxxx")
Convert.ToDecimal("-123").ToString("#,#.##") 结果:-123 Convert.ToDecimal("-12 ...
- C#中给RICHTEXTBOX加上背景图片
在系统自带的RichTextBox中是无法给它设置背景图片,但是我们在某些场合可能需要给RichTextBox设置背景图片.那么怎么实现这一想法呢?经过研究发现通过其它巧妙的途径可以给RichText ...
- String reduction (poj 3401
String reduction Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 1360 Accepted: 447 D ...
- html简单标签代码
html简单标签代码demo <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "ht ...
- php面试专题---12、JavaScript和jQuery基础考点
php面试专题---12.JavaScript和jQuery基础考点 一.总结 一句话总结: 比较常考察的是JavaScript的HTML样式操作以及jQuery的选择器和事件.样式操作. 1.下列不 ...
- codecs模块, decode、encode
使用codecs模块,在Python中完成字符编码 字符的编码是按照某种规则在单字节字符和多字节字符之间进行转换的某种方法.从单字节到多字节叫做decoding,从多字节到单字节叫做encodin ...
- 心形陀螺案例css3
<!DOCTYPE html><html lang="zh-cn"><head> <meta charset="UTF-8&qu ...
- DVBS/S2功能