怎么建设小型网站,整合营销传播方案,环球旅行社网站建设规划书论文,wordpress评论图片#x1f3f7;️个人主页#xff1a;牵着猫散步的鼠鼠 #x1f3f7;️系列专栏#xff1a;Java全栈-专栏 #x1f3f7;️个人学习笔记#xff0c;若有缺误#xff0c;欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站#xff0c;通俗易懂#xff0c;风趣幽默️个人主页牵着猫散步的鼠鼠 ️系列专栏Java全栈-专栏 ️个人学习笔记若有缺误欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站通俗易懂风趣幽默忍不住分享一下给大家。点击跳转到网站AI学习网站。 目录
前言
1.导入Redisson
引入依赖
编写配置
声明Redisson客户端Bean
2.自定义注解
3.AOP切面编程
导入依赖
编写AOP限流代码
4.接口使用自定义注解实现限流
使用自定义限流注解
绑定限流回调函数
总结 前言
在开发高并发系统时有三把利器用来保护系统缓存、降级和限流。 限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统一旦达到限制速率则可以拒绝服务、排队或等待
我们上次讲解了如何使用Sentinel来实现服务限流今天我们来讲解下如何使用RedissonAOP自定义注解反射优雅的实现服务限流本文讲解的限流实现支持针对用户IP限流整个接口的访问限流以及对某个参数字段的限流并且支持请求限流后处理回调 1.导入Redisson
引入依赖
我们首先导入Redisson所需要的依赖我们这里的springboot版本为2.7.12
dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.23.4/version
/dependency
编写配置
# Redisson客户端
redis:sdk:config:host: redis服务IPport: 6379password: redis密码没有可删掉这行pool-size: 10min-idle-size: 5idle-timeout: 30000connect-timeout: 5000retry-attempts: 3retry-interval: 1000ping-interval: 60000keep-alive: true
声明Redisson客户端Bean
配置映射类RedisCientConfigProperties
Data
ConfigurationProperties(prefix redis.sdk.config, ignoreInvalidFields true)
public class RedisCientConfigProperties {/** host:ip */private String host;/** 端口 */private int port;/** 账密 */private String password;/** 设置连接池的大小默认为64 */private int poolSize 64;/** 设置连接池的最小空闲连接数默认为10 */private int minIdleSize 10;/** 设置连接的最大空闲时间单位毫秒超过该时间的空闲连接将被关闭默认为10000 */private int idleTimeout 10000;/** 设置连接超时时间单位毫秒默认为10000 */private int connectTimeout 10000;/** 设置连接重试次数默认为3 */private int retryAttempts 3;/** 设置连接重试的间隔时间单位毫秒默认为1000 */private int retryInterval 1000;/** 设置定期检查连接是否可用的时间间隔单位毫秒默认为0表示不进行定期检查 */private int pingInterval 0;/** 设置是否保持长连接默认为true */private boolean keepAlive true;
}
Configuration
EnableConfigurationProperties(RedisCientConfigProperties.class)
public class RedisClientConfig {Bean(redissonClient)public RedissonClient redissonClient(ConfigurableApplicationContext applicationContext, RedisCientConfigProperties properties) {Config config new Config();// 根据需要可以设定编解码器https://github.com/redisson/redisson/wiki/4.-%E6%95%B0%E6%8D%AE%E5%BA%8F%E5%88%97%E5%8C%96// config.setCodec(new RedisCodec());config.useSingleServer().setAddress(redis:// properties.getHost() : properties.getPort()).setPassword(properties.getPassword()).setConnectionPoolSize(properties.getPoolSize()).setConnectionMinimumIdleSize(properties.getMinIdleSize()).setIdleConnectionTimeout(properties.getIdleTimeout()).setConnectTimeout(properties.getConnectTimeout()).setRetryAttempts(properties.getRetryAttempts()).setRetryInterval(properties.getRetryInterval()).setPingConnectionInterval(properties.getPingInterval()).setKeepAlive(properties.isKeepAlive());RedissonClient redissonClient Redisson.create(config);// 注册消息发布订阅主题Topic// 找到所有实现了Redisson中MessageListener接口的bean名字String[] beanNamesForType applicationContext.getBeanNamesForType(MessageListener.class);for (String beanName : beanNamesForType) {// 通过bean名字获取到监听beanMessageListener bean applicationContext.getBean(beanName, MessageListener.class);Class? extends MessageListener beanClass bean.getClass();// 如果bean的注解里包含我们的自定义注解RedisTopic.class则以RedisTopic注解的值作为name将该bean注册到bean工厂方便在别处注入if (beanClass.isAnnotationPresent(RedisTopic.class)) {RedisTopic redisTopic beanClass.getAnnotation(RedisTopic.class);RTopic topic redissonClient.getTopic(redisTopic.topic());topic.addListener(String.class, bean);ConfigurableListableBeanFactory beanFactory applicationContext.getBeanFactory();beanFactory.registerSingleton(redisTopic.topic(), topic);}}return redissonClient;}static class RedisCodec extends BaseCodec {private final Encoder encoder in - {ByteBuf out ByteBufAllocator.DEFAULT.buffer();try {ByteBufOutputStream os new ByteBufOutputStream(out);JSON.writeJSONString(os, in, SerializerFeature.WriteClassName);return os.buffer();} catch (IOException e) {out.release();throw e;} catch (Exception e) {out.release();throw new IOException(e);}};private final DecoderObject decoder (buf, state) - JSON.parseObject(new ByteBufInputStream(buf), Object.class);Overridepublic DecoderObject getValueDecoder() {return decoder;}Overridepublic Encoder getValueEncoder() {return encoder;}}}
2.自定义注解
我们这里自定义一个注解来作为后续AOP切面编程的切点
根据注解Key属性的值我们会有如下情况
all针对整个接口限流
request_ip针对各个用户的访问IP限流
其他str根据参数作为标识符限流比如我这里keyuserid那么我会根据参数中的userid来限流
Documented
Retention(RetentionPolicy.RUNTIME)
Target({ElementType.TYPE, ElementType.METHOD})
public interface AccessInterceptor {/** 用哪个字段作为拦截标识符配置all则是对整个接口限流配置request_ip* 则是对访问ip限流配置其他str则会到参数中寻找对应名称的属性值包括对象内部属性 */String key() default all;/** 限制频次每秒请求次数 */long permitsPerSecond();/** 黑名单拦截多少次限制后加入黑名单0 不限制 */double blacklistCount() default 0;/** 拦截后的执行方法 */String fallbackMethod();}
3.AOP切面编程
导入依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId
/dependency
编写AOP限流代码
我们doRouter切面函数以AccessInterceptor注解为切点根据注解的各类配置来执行整个限流过程。
我们通过使用Redisson的RRateLimiter限流器基于令牌桶实现访问限流并对已经限流的访问记录黑名单次数超过设置的黑名单阈值就会被加入黑名单中较长时间无法访问
代码较长为了缩短篇幅就一次性放上来了各处已经打上了详细注解若有疑问可评论区留言。
Slf4j
Aspect
public class RateLimiterAOP {// 注入我们声明的redisson客户端Resourceprivate RedissonClient redissonClient;// 限流RateLimiter缓存前缀private static final String rateLimiterName test:RateLimiter:;// 黑名单原子计数器缓存前缀private static final String blacklistPrefix test:RateBlockList:;Around(annotation(accessInterceptor))public Object doRouter(ProceedingJoinPoint jp, AccessInterceptor accessInterceptor) throws Throwable {// 获取注解配置的字段keyString key accessInterceptor.key();if (StringUtils.isBlank(key)) {log.error(限流RateLimiter注解中的 Key 属性为空);throw new RuntimeException(RateLimiter注解中的 Key 属性为空);}log.info(限流拦截关键字为 {}, key);// 根据key获取拦截标识符字段String keyAttr getAttrValue(key, jp.getArgs());// 黑名单拦截非法访问次数超过黑名单阈值if (!all.equals(keyAttr) accessInterceptor.blacklistCount() ! 0 redissonClient.getAtomicLong(blacklistPrefix keyAttr).get() accessInterceptor.blacklistCount()) {log.info(限流-黑名单拦截{}, keyAttr);return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());}// 获取限流器 - Redisson RRateLimiterRRateLimiter rateLimiter redissonClient.getRateLimiter(rateLimiterName keyAttr);if (!rateLimiter.isExists()) {// 创建令牌桶数据模型单位时间内产生多少令牌rateLimiter.trySetRate(RateType.PER_CLIENT,1, accessInterceptor.permitsPerSecond(), RateIntervalUnit.MINUTES);}// 限流判断,没有获取到令牌超出频率if (!rateLimiter.tryAcquire()) {// 如果开启了黑名单限制那么就记录当前的非法访问次数if (accessInterceptor.blacklistCount() ! 0) {RAtomicLong atomicLong redissonClient.getAtomicLong(blacklistPrefix keyAttr);atomicLong.incrementAndGet(); // 原子自增atomicLong.expire(24, TimeUnit.HOURS); // 刷新黑名单原子计数器器过期时间为24小时}log.info(限流-频率过高拦截{}, keyAttr);return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());}// 返回结果return jp.proceed();}/*** 调用用户配置的回调方法使用反射机制实现。*/private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {// 通过JoinPoint对象获取方法的签名SignatureSignature sig jp.getSignature();// 将方法签名转换为MethodSignature对象以便获取方法的详细信息MethodSignature methodSignature (MethodSignature) sig;// 获取到具体的方法对象,通过方法名和参数所以回调函数参数一定要和原方法一致Method method jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());// 调用目标对象的方法并传入当前对象jp.getThis()和方法的参数jp.getArgs()。return method.invoke(jp.getThis(), jp.getArgs());}/*** 根据JoinPoint对象获取其所代表的方法对象*/private Method getMethod(JoinPoint jp) throws NoSuchMethodException {Signature sig jp.getSignature();MethodSignature methodSignature (MethodSignature) sig;return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());}/*** 实际根据自身业务调整主要是为了获取通过某个值做拦截*/public String getAttrValue(String attr, Object[] args) {String filedValue null;for (Object arg : args) {try {// 找到HttpServletRequest对象来获取请求IP地址如果是根据IP拦截的话if (request_ip.equals(attr) arg instanceof HttpServletRequest) {HttpServletRequest request (HttpServletRequest) arg;filedValue IPUtils.getIpAddr(request);}// 找到了值返回if (StringUtils.isNotBlank(filedValue)) {break;}// fix: 使用lombok时uId这种字段的get方法与idea生成的get方法不同会导致获取不到属性值改成反射获取解决filedValue String.valueOf(this.getValueByName(arg, attr));} catch (Exception e) {log.error(获取路由属性值失败 attr{}, attr, e);}}return filedValue;}/*** 获取对象的特定属性值反射** param item 对象* param name 属性名* return 属性值* author tang*/private Object getValueByName(Object item, String name) {try {// 获取指定对象中对应属性名的Field对象Field field getFieldByName(item, name);// 获取到的Field对象为null表示属性不存在直接返回null。if (field null) {return null;}// 将Field对象设置为可访问以便获取私有属性的值。field.setAccessible(true);// 获取属性值并将其赋值给变量o。Object o field.get(item);// 将Field对象设置为不可访问以保持对象的封装性。field.setAccessible(false);return o;} catch (IllegalAccessException e) {return null;}}/*** 根据名称获取方法该方法同时兼顾继承类获取父类的属性** param item 对象* param name 属性名* return 该属性对应方法* author tang*/private Field getFieldByName(Object item, String name) {try {Field field;try {// 获取指定对象中对应属性名的Field对象。field item.getClass().getDeclaredField(name);} catch (NoSuchFieldException e) {// 没有找到抛出NoSuchFieldException异常尝试获取父类中对应属性名的Field对象field item.getClass().getSuperclass().getDeclaredField(name);}return field;} catch (NoSuchFieldException e) {// 父类也没找到对应属性名的Field对象寄返回nullreturn null;}}}
以上代码用到了自己写的一个工具类IPUtils来获取请求的IP地址内容如下
public class IPUtils {private static Logger logger LoggerFactory.getLogger(IPUtils.class);private static final String IP_UTILS_FLAG ,;private static final String UNKNOWN unknown;private static final String LOCALHOST_IP 0:0:0:0:0:0:0:1;private static final String LOCALHOST_IP1 127.0.0.1;/*** 获取IP地址* p* 使用Nginx等反向代理软件 则不能通过request.getRemoteAddr()获取IP地址* 如果使用了多级反向代理的话X-Forwarded-For的值并不止一个而是一串IP地址X-Forwarded-For中第一个非unknown的有效IP字符串则为真实IP地址*/public static String getIpAddr(HttpServletRequest request) {String ip null;try {//以下两个获取在k8s中将真实的客户端IP放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。ip request.getHeader(X-Original-Forwarded-For);if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getHeader(X-Forwarded-For);}//获取nginx等代理的ipif (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getHeader(x-forwarded-for);}if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getHeader(Proxy-Client-IP);}if (StringUtils.isEmpty(ip) || ip.length() 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getHeader(WL-Proxy-Client-IP);}if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getHeader(HTTP_CLIENT_IP);}if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getHeader(HTTP_X_FORWARDED_FOR);}//兼容k8s集群获取ipif (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip request.getRemoteAddr();if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {//根据网卡取本机配置的IPInetAddress iNet null;try {iNet InetAddress.getLocalHost();} catch (UnknownHostException e) {logger.error(getClientIp error: {}, e);}ip iNet.getHostAddress();}}} catch (Exception e) {logger.error(IPUtils ERROR , e);}//使用代理则获取第一个IP地址if (!StringUtils.isEmpty(ip) ip.indexOf(IP_UTILS_FLAG) 0) {ip ip.substring(0, ip.indexOf(IP_UTILS_FLAG));}return ip;}}
4.接口使用自定义注解实现限流
使用自定义限流注解
比如我在用户controller层的登录接口上使用注解key为request_ip表示根据用户IP限流回调函数为fallbackMethod每分钟访问限制10次 PostMapping(value /login)AccessInterceptor(key request_ip, fallbackMethod loginErr, permitsPerSecond 1L, blacklistCount 10)public ResponseString doLogin(RequestParam String code, HttpServletRequest request){
绑定限流回调函数
这里需要注意的是回调函数的参数必须和你使用限流注解的方法参数一致否则报对应方法找不到的错误因为这里是通过反射机制找到回调函数执行的
public ResponseString loginErr(String code, HttpServletRequest request) {System.out.println(限流触发回调参数信息: code);return Response.Stringbuilder().code(Constants.ResponseCode.FREQUENCY_LIMITED.getCode()).info(Constants.ResponseCode.FREQUENCY_LIMITED.getInfo()).data(code).build();}
总结
以上通过Redission自定义注解AOP反射实现了对不同标识符的限流和黑名单拦截并且可以绑定限流回调函数来处理限流后的逻辑代码篇幅较长各位小伙伴也可以尝试继续优化一下这里的设计减少request_ip这种魔法值(实在懒得改了)感谢您的收看万字长文(虽然大部分是代码)有帮助就多多支持吧 文章转载自: http://www.morning.nhdmh.cn.gov.cn.nhdmh.cn http://www.morning.kwqcy.cn.gov.cn.kwqcy.cn http://www.morning.ldsgm.cn.gov.cn.ldsgm.cn http://www.morning.rcyrm.cn.gov.cn.rcyrm.cn http://www.morning.mcgsq.cn.gov.cn.mcgsq.cn http://www.morning.elmtw.cn.gov.cn.elmtw.cn http://www.morning.hyhqd.cn.gov.cn.hyhqd.cn http://www.morning.hpkr.cn.gov.cn.hpkr.cn http://www.morning.rykx.cn.gov.cn.rykx.cn http://www.morning.bljcb.cn.gov.cn.bljcb.cn http://www.morning.hqykb.cn.gov.cn.hqykb.cn http://www.morning.zqmdn.cn.gov.cn.zqmdn.cn http://www.morning.tllhz.cn.gov.cn.tllhz.cn http://www.morning.mlnbd.cn.gov.cn.mlnbd.cn http://www.morning.cjnfb.cn.gov.cn.cjnfb.cn http://www.morning.owenzhi.com.gov.cn.owenzhi.com http://www.morning.mnsts.cn.gov.cn.mnsts.cn http://www.morning.kqxng.cn.gov.cn.kqxng.cn http://www.morning.baohum.com.gov.cn.baohum.com http://www.morning.hxsdh.cn.gov.cn.hxsdh.cn http://www.morning.chgmm.cn.gov.cn.chgmm.cn http://www.morning.bwxph.cn.gov.cn.bwxph.cn http://www.morning.wrlcy.cn.gov.cn.wrlcy.cn http://www.morning.nzhzt.cn.gov.cn.nzhzt.cn http://www.morning.kqgsn.cn.gov.cn.kqgsn.cn http://www.morning.gwkjg.cn.gov.cn.gwkjg.cn http://www.morning.cwznh.cn.gov.cn.cwznh.cn http://www.morning.youyouling.cn.gov.cn.youyouling.cn http://www.morning.lgqdl.cn.gov.cn.lgqdl.cn http://www.morning.npmpn.cn.gov.cn.npmpn.cn http://www.morning.yjxfj.cn.gov.cn.yjxfj.cn http://www.morning.pqcrz.cn.gov.cn.pqcrz.cn http://www.morning.wslr.cn.gov.cn.wslr.cn http://www.morning.zknxh.cn.gov.cn.zknxh.cn http://www.morning.rhpy.cn.gov.cn.rhpy.cn http://www.morning.rlhh.cn.gov.cn.rlhh.cn http://www.morning.lkfhk.cn.gov.cn.lkfhk.cn http://www.morning.kfcz.cn.gov.cn.kfcz.cn http://www.morning.kjfqf.cn.gov.cn.kjfqf.cn http://www.morning.tbqbd.cn.gov.cn.tbqbd.cn http://www.morning.jqkjr.cn.gov.cn.jqkjr.cn http://www.morning.zrfwz.cn.gov.cn.zrfwz.cn http://www.morning.bljcb.cn.gov.cn.bljcb.cn http://www.morning.ssqrd.cn.gov.cn.ssqrd.cn http://www.morning.ktlfb.cn.gov.cn.ktlfb.cn http://www.morning.gccrn.cn.gov.cn.gccrn.cn http://www.morning.rqlbp.cn.gov.cn.rqlbp.cn http://www.morning.msbpb.cn.gov.cn.msbpb.cn http://www.morning.yhpl.cn.gov.cn.yhpl.cn http://www.morning.brrxz.cn.gov.cn.brrxz.cn http://www.morning.lmqfq.cn.gov.cn.lmqfq.cn http://www.morning.nmyrg.cn.gov.cn.nmyrg.cn http://www.morning.nnwnl.cn.gov.cn.nnwnl.cn http://www.morning.wjlnz.cn.gov.cn.wjlnz.cn http://www.morning.sxcwc.cn.gov.cn.sxcwc.cn http://www.morning.ybhrb.cn.gov.cn.ybhrb.cn http://www.morning.fhqdb.cn.gov.cn.fhqdb.cn http://www.morning.jcfdk.cn.gov.cn.jcfdk.cn http://www.morning.rqqkc.cn.gov.cn.rqqkc.cn http://www.morning.trfh.cn.gov.cn.trfh.cn http://www.morning.grwgw.cn.gov.cn.grwgw.cn http://www.morning.hlshn.cn.gov.cn.hlshn.cn http://www.morning.hrzhg.cn.gov.cn.hrzhg.cn http://www.morning.jpydf.cn.gov.cn.jpydf.cn http://www.morning.lnyds.cn.gov.cn.lnyds.cn http://www.morning.xstfp.cn.gov.cn.xstfp.cn http://www.morning.fsjcn.cn.gov.cn.fsjcn.cn http://www.morning.bmgdl.cn.gov.cn.bmgdl.cn http://www.morning.pwsnr.cn.gov.cn.pwsnr.cn http://www.morning.mgkcz.cn.gov.cn.mgkcz.cn http://www.morning.mbprq.cn.gov.cn.mbprq.cn http://www.morning.rwfp.cn.gov.cn.rwfp.cn http://www.morning.xlmgq.cn.gov.cn.xlmgq.cn http://www.morning.ntyks.cn.gov.cn.ntyks.cn http://www.morning.nkqrq.cn.gov.cn.nkqrq.cn http://www.morning.lstmg.cn.gov.cn.lstmg.cn http://www.morning.hmmtx.cn.gov.cn.hmmtx.cn http://www.morning.pdynk.cn.gov.cn.pdynk.cn http://www.morning.hybmz.cn.gov.cn.hybmz.cn http://www.morning.lyjwb.cn.gov.cn.lyjwb.cn