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重启时, ...
随机推荐
- php session之redis存储
前提:redis已安装好. php代码: <?php ini_set("session.save_handler", "redis"); ini_set( ...
- MySQL跑得慢的原因分析
第一点,硬件太老 硬件我们这里主要从CPU.内存.磁盘三个方面来说下,还有一些因素比如网卡,机房网络等因为文章篇幅关系,就不一一介绍了,以后还有机会可以聊. 首先我们来看下MySQL对CPU的利用特点 ...
- HTML计算机代码元素
计算机代码 1 2 3 4 5 6 var person = { firstName:"Bill", lastName:"Gates", ...
- JAVA学习纲要
这份面试题,包含的内容了十九了模块:Java 基础.容器.多线程.反射.对象拷贝.Java Web 模块.异常.网络.设计模式.Spring/Spring MVC.Spring Boot/Spring ...
- 彻底关闭Postprocess
即使场景中没有postprocess volume,场景中也会有默认的postprocess volume效果,如果需要彻底关闭postprocess, 可以使用命令: sg.PostProcessQ ...
- Centos7开机自动启动服务和联网
虚拟机设置选择NAT模式,默认情况下,Centos不是自动连接上网的,需要点击右上角,手动连接上网. 可以修改开机启动配置修改: 1. cd 到/etc/sysconfig/network-scrip ...
- oracle 数据迁移之数据泵的基本使用
oracle相同数据库下跨schema的表迁移—expdp/impdp 需求:将GUIDO用户下的表迁移到SCOTT用户下 select * from dba_role_privs where GRA ...
- 使用代理IP、高匿IP、连接失败
先百度一下,什么是代理IP 我们使用代理IP就是因为某些站点会屏蔽我们的IP,所以我们要动态的更换代理IP. 代理IP: 其中我们首先选择国内的IP,国外的一般都比较慢,其次不要选择如{新疆乌鲁木齐} ...
- SQLiteDatabase 数据库使用
0 SQLiteDatabases数据库特点 一种切入式关系型数据库,支持事务,可使用SQL语言,独立,无需服务.程序内通过类名可访问数据库,程序外不可以访问. SQLiteDatabases数据库使 ...
- Note-Git:Git 笔记
ylbtech-Note-Git:Git 笔记 1.返回顶部 · Git 分支管理: 主干/master.热修正/hotfix.预生产/release.开发develop.个人1(个人.小团队)/f ...