【防止重复提交】Redis + AOP + 注解的方式实现分布式锁

文章目录

  • 工作原理
  • 需求实现
    • 1)自定义防重复提交注解
    • 2)定义防重复提交AOP切面
    • 3)RedisLock 工具类
    • 4)过滤器 + 请求工具类
    • 5)测试Controller
    • 6)测试结果

工作原理

分布式环境下,可能会遇到用户对某个接口被重复点击的场景,为了防止接口重复提交造成的问题,可用 Redis 实现一个简单的分布式锁来解决问题。

在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

需求实现

  1. 自定义一个防止重复提交的注解,注解中可以携带到期时间和一个参数的key
  2. 为需要防止重复提交的接口添加注解
  3. 注解AOP会拦截加了此注解的请求,进行加解锁处理并且添加注解上设置的key超时时间
  4. Redis 中的 key = token + "-" + path + "-" + param_value; (例如:17800000001 + /api/subscribe/ + zhangsan)
  5. 如果重复调用某个加了注解的接口且key还未到期,就会返回重复提交的Result。

1)自定义防重复提交注解

自定义防止重复提交注解,注解中可设置 超时时间 + 要扫描的参数(请求中的某个参数,最终拼接后成为Redis中的key)

package com.lihw.lihwtestboot.noRepeatSubmit;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)
public @interface NoRepeatSubmit {/*** 锁过期的时间*/int seconds() default 5;/*** 要扫描的参数*/String scanParam() default "";
}

2)定义防重复提交AOP切面

@Pointcut("@annotation(noRepeatSubmit)") 表示切点表达式,它使用了注解匹配的方式来选择被注解 @NoRepeatSubmit 标记的方法。

package com.lihw.lihwtestboot.noRepeatSubmit;import com.alibaba.fastjson.JSONObject;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.UUID;
/*** 重复提交aop*/
@Aspect
@Component
public class RepeatSubmitAspect {private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);@Autowiredprivate RedisLock redisLock;@Pointcut("@annotation(noRepeatSubmit)")public void pointCut(NoRepeatSubmit noRepeatSubmit) {}@Around("pointCut(noRepeatSubmit)")public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {//获取基本信息ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();Assert.notNull(request, "request can not null");int lockSeconds = noRepeatSubmit.seconds();//过期时间String threadName = Thread.currentThread().getName();// 获取当前线程名称String param = noRepeatSubmit.scanParam();//请求参数String path = request.getServletPath();String type = request.getMethod();String param_value = "";if (type.equals("POST")){param_value = JSONObject.parseObject(new BodyReaderHttpServletRequestWrapper(request).getBodyString()).getString(param);}else if (type.equals("GET")){param_value = request.getParameter(param);}String token = request.getHeader("uid");LOGGER.info("线程:{}, 接口:{},重复提交验证",threadName,path);String key;if (!"".equals(param) && param != null){key = token + "-" + path + "-" + param_value;//生成key}else {key = token + "-" + path;//生成key}String clientId = getClientId();// 调接口时生成临时value(UUID)// 用于添加锁,如果添加成功返回true,失败返回false boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);ApiResult result = new ApiResult();if (isSuccess) {LOGGER.info("加锁成功:接口 = {}, key = {}", path, key);// 获取锁成功Object obj;try {// 执行进程obj = pjp.proceed();// aop代理链执行的方法} finally {// 据key从redis中获取valueif (clientId.equals(redisLock.get(key))) {// 解锁redisLock.releaseLock(key, clientId);LOGGER.info("解锁成功:接口={}, key = {},",path, key);}}return obj;} else {// 添加锁失败,认为是重复提交的请求LOGGER.info("重复请求:接口 = {}, key = {}",path, key);result.setData("重复提交");return result;}}private String getClientId() {return UUID.randomUUID().toString();}public static String getRequestBodyData(HttpServletRequest request) throws IOException{BufferedReader bufferReader = new BufferedReader(request.getReader());StringBuilder sb = new StringBuilder();String line = null;while ((line = bufferReader.readLine()) != null) {sb.append(line);}return sb.toString();}
}

3)RedisLock 工具类

package com.lihw.lihwtestboot.noRepeatSubmit;import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;@Service
public class RedisLock {private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);/**  不设置过期时长 */public final static long NOT_EXPIRE = -1;@Autowiredprivate StringRedisTemplate redisTemplate;/*** @param lockKey   加锁键* @param clientId  加锁客户端唯一标识(采用UUID)* @param seconds   锁过期时间* @return*/public boolean tryLock(String lockKey, String clientId, long seconds) {if (redisTemplate.opsForValue().setIfAbsent(lockKey, clientId,seconds, TimeUnit.SECONDS)) {return true;//得到锁}else{return false;}}/*** 与 tryLock 相对应,用作释放锁** @param lockKey* @param clientId* @return*/public boolean releaseLock(String lockKey, String clientId) {String currentValue = redisTemplate.opsForValue().get(lockKey);try {if (!StringUtils.isEmpty(currentValue) && currentValue.equals(clientId)) {redisTemplate.opsForValue().getOperations().delete(lockKey);return true;}else {return false;}} catch (Exception e) {logger.error("解锁异常,,{}" , e);return false;}}/*** 获取* @param key* @return*/public String get(String key) {return get(key, NOT_EXPIRE);}public String get(String key, long expire) {String value = redisTemplate.opsForValue().get(key);if(expire != NOT_EXPIRE){redisTemplate.expire(key, expire, TimeUnit.SECONDS);}return value;}/*** 删除* @param key*/public void delete(String key) {redisTemplate.delete(key);}
}

4)过滤器 + 请求工具类

Filter类

package com.lihw.lihwtestboot.noRepeatSubmit;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.ServletComponentScan;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;@ServletComponentScan
@WebFilter(urlPatterns = "/*",filterName = "channelFilter")
public class ChannelFilter implements Filter {private final Logger logger = LoggerFactory.getLogger(this.getClass());@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {logger.info("-----------------------Execute filter start---------------------");// 防止流读取一次后就没有了, 所以需要将流继续写出去HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);filterChain.doFilter(requestWrapper, servletResponse);}}

BodyReaderHttpServletRequestWrapper

对GET和POST请求的获取参数方法进行了封装

package com.lihw.lihwtestboot.noRepeatSubmit;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{/*** Request请求参数获取处理类*/private final byte[] body;public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {super(request);String sessionStream = getBodyString(request);body = sessionStream.getBytes(StandardCharsets.UTF_8);}/*** 获取请求Body** @param request* @return*/private String getBodyString(final ServletRequest request) {StringBuilder sb = new StringBuilder();InputStream inputStream = null;BufferedReader reader = null;try {inputStream = cloneInputStream(request.getInputStream());reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));String line = "";while ((line = reader.readLine()) != null) {sb.append(line);}} catch (IOException e) {e.printStackTrace();} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}if (reader != null) {try {reader.close();} catch (IOException e) {e.printStackTrace();}}}return sb.toString();}public String getBodyString() {return new String(body, StandardCharsets.UTF_8);}/*** Description: 复制输入流** @param inputStream* @return*/public InputStream cloneInputStream(ServletInputStream inputStream) {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int len;try {while ((len = inputStream.read(buffer)) > -1) {byteArrayOutputStream.write(buffer, 0, len);}byteArrayOutputStream.flush();} catch (IOException e) {e.printStackTrace();}InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());return byteArrayInputStream;}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}
}

5)测试Controller

package com.lihw.lihwtestboot.noRepeatSubmit;import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;@RestController
@RequestMapping("/api")
@Validated
public class noRepeatSubmitController {@GetMapping("/subscribe/{channel}")@NoRepeatSubmit(seconds = 10,scanParam = "username")public ApiResult subscribe(@RequestHeader(name = "uid") String phone,@RequestHeader(name = "username") String username,@PathVariable("channel") @NotEmpty(message = "channel不能为空") String channel) {System.out.println("phone=" + phone);System.out.println("username=" + username);System.out.println("channel=" + channel);try {Thread.sleep(5000);//模拟耗时} catch (InterruptedException e) {e.printStackTrace();}return new ApiResult("success","data");}
}

6)测试结果

重复点击

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

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

相关文章

【资料分享】基于单片机大气压监测报警系统电路方案设计、基于飞思卡尔的无人坚守点滴监控自动控制系统设计(程序,原理图,pcb,文档)

基于单片机大气压监测报警系统电路方案设计 功能:实现的是大气压检测报警系统,可以通过传感器实时检测当前大气压值,可以设定大气压正常范围,当超过设定范围进行报警提示。 资料:protues仿真,程序&#x…

从头开始构建和训练 Transformer(下)

导 读 上一篇推文从头开始构建和训练 Transformer(上)https://blog.csdn.net/weixin_46287760/article/details/136048418介绍了构建和训练Transformer的过程和构建每个组件的代码示例。本文将使用数据对该架构进行代码演示,验证其模型性能。…

2-1 动手学深度学习v2-Softmax回归-笔记

回归 VS 分类 回归估计一个连续值分类预测一个离散类别 从回归到多类分类 回归 单连续数值输出输出的区间:自然区间 R \mathbb{R} R损失:跟真实值的区别 分类 通常多个输出(这个输出的个数是等于类别的个数)输出的第 i i i…

MATLAB知识点:矩阵的除法

​讲解视频:可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇(数学建模清风主讲,适合零基础同学观看)_哔哩哔哩_bilibili 节选自第3章 3.4.2 算术运算 下面我们再来介绍矩阵的除法。事…

企业数字化转型面临什么挑战?

数字化转型是一个复杂且持续的过程,涉及将数字技术集成到组织的各个方面,从根本上改变组织的运营方式和为客户提供价值的方式。虽然具体的挑战可能因企业的性质和规模而异,但一些常见的挑战包括: 1.抵制变革: 文化阻…

Java入门之JavaSe(韩顺平p1-p?)

学习背景: 本科搞过一段ACM、研究生搞了一篇B会后,本人在研二要学Java找工作啦~~(宇宙尽头是Java?)爪洼纯小白入门,C只会STL、python只会基础Pytorch、golang参与了一个Web后端项目,可以说项目小…

Flink-CDC实时读Postgresql数据

前言 CDC,Change Data Capture,变更数据获取的简称,使用CDC我们可以从数据库中获取已提交的更改并将这些更改发送到下游,供下游使用。这些变更可以包括INSERT,DELETE,UPDATE等。 用户可以在如下的场景使用cdc: 实时数据同步:比如将Postgresql库中的数据同步到我们的数仓中…

Python初学者学习记录——python基础综合案例:数据可视化——动态柱状图

一、案例效果 通过pyecharts可以实现数据的动态显示,直观的感受1960~2019年世界各国GDP的变化趋势 二、通过Bar构建基础柱状图 反转x轴和y轴 标签数值在右侧 from pyecharts.charts import Bar from pyecharts.options import LabelOpts# 构建柱状图对象 bar Bar()…

二进制安全虚拟机Protostar靶场(7)heap2 UAF(use-after-free)漏洞

前言 这是一个系列文章&#xff0c;之前已经介绍过一些二进制安全的基础知识&#xff0c;这里就不过多重复提及&#xff0c;不熟悉的同学可以去看看我之前写的文章 heap2 程序静态分析 https://exploit.education/protostar/heap-two/#include <stdlib.h> #include &…

环境配置:Ubuntu18.04 ROS Melodic安装

前言 不同版本的Ubuntu与ROS存在对应关系。 ROS作为目前最受欢迎的机器人操作系统&#xff0c;其核心代码采用C编写&#xff0c;并以BSD许可发布。ROS起源于2007年&#xff0c;是由斯坦福大学与机器人技术公司Willow Garage合作的Switchyard项目。2012年&#xff0c;ROS团队从…

力扣面试题 05.03. 翻转数位(前、后缀和)

Problem: 面试题 05.03. 翻转数位 文章目录 题目描述思路及解法复杂度Code 题目描述 思路及解法 1.将十进制数转换为二进制数&#xff08;每次按位与1求与&#xff0c;并且右移&#xff09;&#xff1b; 2.依次求取二进制数中每一位的前缀1的数量和&#xff0c;和后缀1的数量和…

计算机项目SpringBoot项目 办公小程序开发

从零构建后端项目、利用UNI-APP创建移动端项目 实现注册与登陆、人脸考勤签到、实现系统通知模块 实现会议管理功能、完成在线视频会议功能、 发布Emos在线办公系统 项目分享&#xff1a; SpringBoot项目 办公小程序开发https://pan.baidu.com/s/1sYPLOAMtaopJCFHAWDa2xQ?…

极狐GitLab 使用阿里云作为 OmniAuth 身份验证 provider

使用阿里云作为 OmniAuth 身份验证 provider 您可以启用阿里云 OAuth 2.0 OmniAuth provider并使用您的阿里云账户登录极狐GitLab。 创建阿里云应用 登录阿里云平台&#xff0c;在上面创建一个应用。阿里云会生成一个 client ID and secret key 供您使用。 登录到阿里云平台…

PHP实现DESede/ECB/PKCS5Padding加密算法兼容Java SHA1PRNG

这里写自定义目录标题 背景JAVA代码解决思路PHP解密 背景 公司PHP开发对接一个Java项目接口&#xff0c;接口返回数据有用DESede/ECB/PKCS5Padding加密&#xff0c;并且key也使用了SHA1PRNG加密了&#xff0c;网上找了各种办法都不能解密&#xff0c;耗了一两天的时间&#xf…

C语言:内存函数

创作不易&#xff0c;友友们给个三连吧&#xff01;&#xff01; C语言标准库中有这样一些内存函数&#xff0c;让我们一起学习吧&#xff01;&#xff01; 一、memcpy函数的使用和模拟实现 void * memcpy ( void * destination, const void * source, size_t num ); 1.1 使…

微信小程序(三十四)搜索框-带历史记录

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.搜索框基本模板 2.历史记录基本模板 3.细节处理 源码&#xff1a; index.wxml <!-- 1.点击搜索按钮a.非空判断b.历史记录&#xff08;去重&#xff09;c.清空搜索框d.去除前后多余空格2.删除搜索 3.无搜索…

Golang 学习(一)基础知识

面向对象 Golang 也支持面向对象编程(OOP)&#xff0c;但是和传统的面向对象编程有区别&#xff0c;并不是纯粹的面向对象语言。 Golang 没有类(class)&#xff0c;Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位&#xff0c;Golang 是基于 struct 来实现 OOP…

部署 Zabbix 监控平台

部署 Zabbix 监控平台 目录 部署 Zabbix 监控平台一、 Zabbix简介Zabbix 特性Zabbix监控功能 二、Zabbix 概述Server数据库Web 界面ProxyAgent数据流Zabbix serverZabbix agentzabbix配置文件 三、部署Zabbix1&#xff1a;部署监控服务器1.1安装 LNMP 环境1.2 修改 Nginx 配置文…

Unity类银河恶魔城学习记录1-14 AttackDirection源代码 P41

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili PlayerPrimaryAttackState.cs using System.Collections; using System.Co…

C语言的malloc(0)问题

malloc(0)详解 首先来解释malloc&#xff08;0&#xff09;的问题&#xff0c;这个语法是对的&#xff0c;而且确实也分配了内存&#xff0c;但是内存空间是0&#xff0c;就是说返回给你的指针是不能用的&#xff0c;感觉奇怪吧&#xff1f;但是从操作系统的原理来解释就不奇怪…