RedisTemplate自增时保证原子性的lua脚本限制接口请求频率

场景:限制请求后端接口的频率,例如1秒钟只能请求次数不能超过10次,通常的写法是:

1.先去从redis里面拿到当前请求次数

2.判断当前次数是否大于或等于限制次数

3.当前请求次数小于限制次数时进行自增

这三步在请求不是很密集的时候,程序执行很快,可能不会产生问题,如果两个请求几乎在同一时刻到来,我们第1步和第2步的判断是无法保证原子性的。

改进方式:使用redis的lua脚本,将"读取值、判断大小、自增"放到redis的一次操作中,redis底层所有的操作请求都是串行的,也就是一个请求执行完,才会执行下一个请求。

自增的lua脚本如下

    /*** 自增过期时间的原子性脚本*/private String maxCountScriptText() {return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +"    return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +"    redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";}

 将接口限流功能封装成一个注解@RateLimiter,在接口方法上面加上@RateLimiter就可以实现限流:

 redis工具类:

package com.zhou.redis.util;import com.zhou.redis.dto.MyRedisMessage;
import com.zhou.redis.exception.LockException;
import com.zhou.redis.script.MaxCountQueryScript;
import com.zhou.redis.script.MaxCountScript;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;@Configuration
@Slf4j
public class RedisUtil {public RedisTemplate<String, Object> redisTemplate;private MaxCountScript maxCountScript;private MaxCountQueryScript maxCountQueryScript;public RedisUtil(RedisTemplate redisTemplate, MaxCountScript maxCountScript, MaxCountQueryScript maxCountQueryScript) {this.redisTemplate = redisTemplate;this.maxCountScript = maxCountScript;this.maxCountQueryScript = maxCountQueryScript;}/***  尝试加锁,返回加锁成功或者失败* @param time 秒**/public boolean tryLock(String key,Object value,Long time){if(time == null || time <= 0){time = 30L;}Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(time));return b == null ? false : b;}/***  释放锁(拿到锁之后才能调用释放锁)**/public boolean unLock(String key){Boolean b = redisTemplate.delete(key);return b == null ? false : b;}/*** 对key进行自增1* @param maxCount  最大值* @param time      增加次数* @return          自增后的值*/public Long incr(String key,int maxCount, int time){List<String> keys = Collections.singletonList(key);return  redisTemplate.execute(maxCountScript, keys, maxCount, time);}/*** 获得当前值*/public Long incrNow(String key){List<String> keys = Collections.singletonList(key);return  redisTemplate.execute(maxCountQueryScript, keys);}
}

 redis配置类:

package com.zhou.redis.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhou.redis.listener.MyRedisListener;
import com.zhou.redis.script.MaxCountQueryScript;
import com.zhou.redis.script.MaxCountScript;
import com.zhou.redis.util.RedisTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.util.Arrays;
import java.util.List;@Configuration
public class RedisConfig {@SuppressWarnings("all")@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);//Json序列化配置Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);//String的序列化StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();//key采用string的序列化template.setKeySerializer(stringRedisSerializer);//hash的key采用string的序列化template.setHashKeySerializer(stringRedisSerializer);//value序列化采用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);//hash的value序列化方式采用jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}/*** Redis消息监听器容器* 这个容器加载了RedisConnectionFactory和消息监听器* 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器* 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理** @param redisConnectionFactory 连接工厂* @param adapter                适配器* @return redis消息监听容器*/@Bean@SuppressWarnings("all")public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory,FuncUpdateListener listener,MessageListenerAdapter adapter) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();// 监听所有库的key过期事件container.setConnectionFactory(redisConnectionFactory);// 所有的订阅消息,都需要在这里进行注册绑定,new PatternTopic(TOPIC_NAME1)表示发布的主题信息// 可以添加多个 messageListener,配置不同的通道List<Topic> topicList = Arrays.asList(new PatternTopic(RedisTopic.TOPIC1),new PatternTopic(RedisTopic.TOPIC2));container.addMessageListener(listener, topicList);/*** 设置序列化对象* 特别注意:1. 发布的时候需要设置序列化;订阅方也需要设置序列化*         2. 设置序列化对象必须放在[加入消息监听器]这一步后面,否则会导致接收器接收不到消息*/Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);seria.setObjectMapper(objectMapper);container.setTopicSerializer(seria);return container;}/*** 这个地方是给messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“receiveMessage”* 也有好几个重载方法,这边默认调用处理器的方法 叫OnMessage*/@SuppressWarnings("all")@Beanpublic MessageListenerAdapter listenerAdapter() {//MessageListenerAdapter receiveMessage = new MessageListenerAdapter(printMessageReceiver, "receiveMessage");MessageListenerAdapter receiveMessage = new MessageListenerAdapter();Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);seria.setObjectMapper(objectMapper);receiveMessage.setSerializer(seria);return receiveMessage;}@Beanpublic MaxCountScript maxCountScript() {return new MaxCountScript(maxCountScriptText());}@Beanpublic MaxCountQueryScript maxCountQueryScript() {return new MaxCountQueryScript(maxCountQueryScriptText());}/*** 自增过期时间的原子性脚本*/private String maxCountScriptText() {return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +"    return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +"    redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";/*return "local limitMaxCount = tonumber(ARGV[1])\n" +"local limitSecond = tonumber(ARGV[2])\n" +"local num = tonumber(redis.call('get', KEYS[1]) or '-1')\n" +"if limitMaxCount then\n" +"   return -1\n" +"end\n" +"if num == -1 then\n" +"    redis.call('incr', KEYS[1])\n" +"    redis.call('expire', KEYS[1], limitSecond)\n" +"    return 1\n" +"else\n" +"    if num >= limitMaxCount then\n" +"        return 0\n" +"    else\n" +"        redis.call('incr', KEYS[1])\n" +"        return 1\n" +"    end\n" +"end";*/}/*** 查询当前值脚本*/private String maxCountQueryScriptText() {return "local key = KEYS[1]\n" +"local current = redis.call('get', key);\n" +"if current then\n" +"    return tonumber(current);\n" +"else\n" +"    return current\n" +"end\n";}
}

 拦截模式枚举类:根据ip拦截或者方法拦截

package com.zhou.aop;/*** @author lang.zhou* @since 2023/1/31 17:56*/
public enum LimitType {IP,DEFAULT
}

 封装自定义注解:@RateLimiter

package com.zhou.aop;import java.lang.annotation.*;/*** @author lang.zhou* @since 2023/1/31 17:49*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {/*** 限流key*/String key() default "RateLimiter";/*** 限流时间,单位秒*/int time() default 60;/*** 限流次数*/int count() default 100;/*** 限流类型*/LimitType limitType() default LimitType.DEFAULT;/*** 限流后返回的文字*/String limitMsg() default "访问过于频繁,请稍候再试";
}

 注解的切面逻辑:

package com.zhou.aop;import com.zhou.redis.util.RedisUtil;
import com.zhou.common.utils.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;/*** 接口限流切面* @author lang.zhou* @since 2023/1/31 17:50*/
@Aspect
@Slf4j
@Component
public class RateLimiterAspect {@Autowiredprivate RedisUtil redisUtils;@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) {int time = rateLimiter.time();int count = rateLimiter.count();String combineKey = getCombineKey(rateLimiter, point);try {Long number = redisUtils.incr(combineKey, count, time);if (number == null || number.intValue() > count){log.info("请求【{}】被拦截,{}秒内请求次数{}",combineKey,time,number);throw new RuntimeException(rateLimiter.limitMsg());}} catch (ServiceRuntimeException e) {throw e;} catch (Exception e) {throw new RuntimeException("网络繁忙,请稍候再试");}}/*** 获取限流key*/public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {StringBuilder s = new StringBuilder(rateLimiter.key());ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if(requestAttributes != null){HttpServletRequest request = requestAttributes.getRequest();if (rateLimiter.limitType() == LimitType.IP) {s.append(IpUtil.getIpAddr(request)).append("-");}}MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();s.append(targetClass.getName()).append(".").append(method.getName());return s.toString();}}

 lua自增脚本类:

package com.zhou.redis.script;import org.springframework.data.redis.core.script.DefaultRedisScript;/*** @author lang.zhou* @since 2023/2/25*/
public class MaxCountScript extends DefaultRedisScript<Long> {public MaxCountScript(String script) {super(script,Long.class);}
}

 lua查询当前值的脚本类:

package com.zhou.redis.script;import org.springframework.data.redis.core.script.DefaultRedisScript;/*** @author lang.zhou* @since 2023/2/25*/
public class MaxCountQueryScript extends DefaultRedisScript<Long> {public MaxCountQueryScript(String script) {super(script,Long.class);}
}

 订阅消息通道的枚举:

package com.zhou.redis.util;public class RedisTopic {public static final String TOPIC1 = "TOPIC1";public static final String TOPIC2 = "TOPIC2";
}

消息实体类: 

package com.zhou.redis.dto;import lombok.Data;import java.io.Serializable;/*** redis订阅消息实体* @since 2022/11/11 17:34*/
@Data
public class MyRedisMessage implements Serializable {private String msg;
}

订阅消息监听器: 

package com.zhou.redis.listener;import com.zhou.redis.dto.MyRedisMessage;
import com.zhou.redis.util.RedisTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import javax.script.ScriptException;/*** @author lang.zhou*/
@Slf4j
@Component
public class MyRedisListener implements MessageListener {@Autowiredprivate RedisTemplate<String,Object> redisTemplate;@Overridepublic void onMessage(Message message, byte[] pattern) {String topic = new String(pattern);// 接收的topiclog.info("channel:{}" , topic);if(RedisTopic.TOPIC1.equals(topic)){//}else if(RedisTopic.TOPIC2.equals(topic)){//序列化对象(特别注意:发布的时候需要设置序列化;订阅方也需要设置序列化)MyRedisMessage msg = (MyRedisMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());log.info("message:{}",msg);}}
}

注解使用方式:1秒内一个ip最多只能请求10次

@RestController
@RequestMapping("/test/api")
public class CheckController{@PostMapping("/limit")@RateLimiter(time = 1, count = 10, limitType = LimitType.IP, limitMsg = "请求过于频繁,请稍后重试")public void limit(HttpServletRequest request){//执行业务代码}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/588685.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

yarn run dev运行ant design pro项目报错-‘max‘ 不是内部或外部命令

运行ant design pro项目报错&#xff1a; >>yarn run dev yarn run v1.22.19 $ npm run start:dev > ant-design-pro6.0.0-beta.1 start:dev > cross-env REACT_APP_ENVdev MOCKnone UMI_ENVdev max dev max 不是内部或外部命令&#xff0c;也不是可运行的程序 …

PyTorch中常用的工具(4)Visdom

文章目录 前言3.2 Visdom 前言 在训练神经网络的过程中需要用到很多的工具&#xff0c;最重要的是数据处理、可视化和GPU加速。本章主要介绍PyTorch在这些方面常用的工具模块&#xff0c;合理使用这些工具可以极大地提高编程效率。 由于内容较多&#xff0c;本文分成了五篇文…

影视后期: PR调色处理,调色工具面板介绍

写在前面 整理一些影视后期的相关笔记博文为 Pr 调色处理&#xff0c;涉及调色工具面板简单认知包括 lumetri 颜色和范围面板理解不足小伙伴帮忙指正 元旦快乐哦 _ 名词解释 饱和度 是指色彩的鲜艳程度&#xff0c;也被称为色彩的纯度。具体来说&#xff0c;它表示色相中灰色…

从马尔可夫奖励过程到马尔可夫决策到强化学习【01/2】

一、说明 关于马尔可夫过程&#xff0c;如何将马尔可夫决策转化成决策依据&#xff0c;这里介绍的基本的思想路径&#xff0c;为读者将来设计和应用决策模型提供理论上的参考。 这是了解强化学习的一系列基础文章的后续文章。如果您有兴趣了解强化学习&#xff0c;请查看此处。…

运维系列Nginx:设置黑/白名单IP限制

黑/白名单IP限制访问配置 第一种&#xff1a;deny和allow指令属于ngx_http_access_module&#xff0c;nginx默认加载此模块&#xff0c;所以可直接使用。这种方式&#xff0c;最简单&#xff0c;最直接。设置类似防火墙iptable&#xff0c;使用方法&#xff1a; # 白名单设置&a…

【数值分析】choleskey分解,matlab实现

平方根分解&#xff08;Choleskey分解&#xff09; A G G T , A 对称正定 AGG^ \mathrm T \,\,,\,\, A对称正定 AGGT,A对称正定 A L D M L D L T ( L D 1 / 2 ) ( L D 1 / 2 ) T G G T \begin{align*} A LDM LDL^ \mathrm T(LD^{1/2})(LD^{1/2})^ \mathrm TGG^ \mathrm T…

huggingface的tokenizer解读

文章目录 前言一、huggingface的tokenizer含义1、含义2、整体概括 二、加载lmsys/vicuna-7b-v1.5模型的tokenizer三、调用tokernizer方法四、字符串的tokens应用1、tokenizer应用2、tokenizer进行token分词(tokenizer.tokenize)3、tokens转模型识别ids(tokenizer.convert_token…

STM32F407-14.3.10-表73具有有断路功能的互补通道OCx和OCxN的输出控制位-00x10

如上表所示&#xff0c;MOE0&#xff0c;OSSI0&#xff0c;CCxE1&#xff0c;CCxNE0时&#xff0c;OCx与OCxN的输出状态取决于GPIO端口上下拉状态。 ---------------------------------------------------------------------------------------------------------------------…

Python 中的运算符介绍(1)

算数运算符 常见的% 、//、/ 用法 赋值运算符 赋值运算&#xff1a;将等号右边赋值给等号左边 常见场景&#xff1a; 比较运算符 代码解析&#xff1a; 逻辑运算符 位运算符&#xff08;了解&#xff09; 三目运算符 身份证运算符 成员运算符

考研后SpringBoot复习2—容器底层相关注解

考研后SpringBoot复习2 SpringBoot底层注解学习 与容器功能相关的注解与springboot的底层原理密切相关 组件添加注解configuration Spring Ioc容器部分回顾 包括在配置中注册&#xff0c;开启包扫描和注解驱动开发等需要在进行重新的学习回顾 实例 package com.dzu.boot;imp…

zyqn-arm软中断设置

所有SGI都是边缘触发的&#xff0c;sgi的灵敏度类型是固定的&#xff0c;不能改变。 软中断初始化流程 1、初始化异常处理 2、初始化中断控制器 3、注册异常处理回调函数到CPU 4、连接软中断信号与注册软中断回调函数 5、使能中断控制器中的软中断中断 6、使能异常处理 …

Android 10.0 mtp模式下PC上显示两个内部存储的问题解决办法

1. 前言 在10.0的系统产品开发中,在mtp模式下,有些时候会在pc端显示两个手机内部存储空间,所以这时候显得特别不友好,出于对产品 的完善,所以要求解决这个问题,接下来分析下这个问题 2.mtp模式下PC上显示两个内部存储的问题解决办法的核心类 frameworks\base\media\jav…

k8s学习 — 各章节重要知识点

k8s学习 — 各章节重要知识点 学习资料k8s版本0 相关命令0.1 yaml配置文件中粘贴内容格式混乱的解决办法0.2 通用命令0.3 Node 相关命令0.4 Pod 相关命令0.5 Deployment 相关命令0.6 Service 相关命令0.7 Namespace 相关命令 1 k8s学习 — 第一章 核心概念1.1 Pod、Node、Servi…

Python装饰器的专业解释

装饰器&#xff0c;其实是用到了闭包的原理来进行操作的。 单个装饰器&#xff1a; 以下是一个简单的例子&#xff1a; def outer(func):print("OUTER enter ...")def wrapper(*args, **kwargs):print("调用之前......")result func(*args, **kwargs)p…

【Vue2+3入门到实战】(17)VUE之VueCli脚手架自定认创建项目、ESlint代码规范与修复、 ESlint自动修正插件的使用 详细示例

目录 一、本节内容二、VueCli 自定义创建项目三、ESlint代码规范及手动修复1.JavaScript Standard Style 规范说明2.代码规范错误3.手动修正 四、通过eslint插件来实现自动修正 一、本节内容 VueCli脚手架自定认创建项目ESlint代码规范与修复ESlint自动修正插件 二、VueCli 自…

FA模板制作流程

1、FA模板制作的流程&#xff08;完整复制模板制作&#xff09; 总结&#xff1a; FA完整复制云桌面模板流程&#xff1a; 1、安装一个全新的Windows&#xff0c;挂载并安装tools 2、关闭防火墙、启动administrator本地超管用户 3、挂载FusionAccess_WindowsDesktop_Instal…

使用iris数据集学习基础的机器学习分类任务

通过一个案例来理解机器学习的分类任务 关于数据集的相关介绍大家可以观看下面的连接&#xff1a; iris数据集的介绍 根据数据集的特征值来预测目标值 即通过x来预测y 相应的代码如下&#xff1a; # Training Machine Learning Algorithms for Classification # 加载数据 s h…

C/C++面向对象(OOP)编程-回调函数详解(回调函数、异步编程、函数指针、)

本文主要介绍回调函数的使用&#xff0c;包括函数指针、异步回调编程、主要通过详细的例子来指导在异步编程和事件编程中如何使用回调函数来实现。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;C/C精进之路 &…

黑马程序员SSM框架-SpringBoot

视频连接&#xff1a;SpringBoot-01-SpringBoot工程入门案例开发步骤_哔哩哔哩_bilibili SpringBoot简介 入门程序 也可以基于官网创建项目。 SpringBoot项目快速启动 下面的插件将项目运行所需的依赖jar包全部加入到了最终运行的jar包中&#xff0c;并将入口程序指定。 Spri…

【网络面试(2)】DNS原理-域名和IP地址的查询转换

从上一篇博客我们得知浏览器是如何生成了HTTP消息了&#xff0c;但是浏览器作为应用程序&#xff0c;是不具备向网络中发送请求的能力&#xff0c;而是需要委托给操作系统的内核协议栈来发送请求。在委托协议栈之前&#xff0c;浏览器还要做的一件事情就是将域名转换为IP地址。…