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

news2024/11/19 14:28:31

文章目录

  • 工作原理
  • 需求实现
    • 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);

    @Autowired
    private 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中获取value
                if (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;

    @Autowired
    private 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());

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public 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;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public 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.coloradmin.cn/o/1434880.html

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

相关文章

有趣的CSS - 按钮文字上下滑动

目录 整体效果核心代码html 代码css 部分代码 完整代码如下html 页面css 样式页面渲染效果 整体效果 这个按钮效果主要使用 :hover 伪选择器以及 transition 过渡属性来实现两个子元素上下过渡的效果。 此效果可以在主入口按钮、详情或者更多等按钮处使用,增加一些鼠…

Express框架介绍—node.js

Express—Node.js 官网传送门(opens new window) 基于 Node.js 平台,快速、开放、极简的 Web 开发框架 Express 是用于快速创建服务器的第三方模块。 Express 初体验 基本使用 安装 Express: npm install express创建服务器,监听客户端请…

前端实现搜索框筛选

效果图 页面解析 是一个input输入框和一个button按钮组成输入框查询 内容是一个折叠面板 html代码 <div class"left-content-box"><div class"colum-search"><el-input v-model"columKey" clearable placeholder"请输入关…

第二届 N1CTF Junior Crypto-junior RSA WP

题目&#xff1a; from Crypto.Util.number import * from secret import flagm bytes_to_long(flag)def gen(bits):while True:a getPrime(bits)b getPrime(bits)c getPrime(bits)p (a << (2*bits)) (b << bits) cq (c << (2*bits)) (a << …

Day4.

单链表 #include <head.h>typedef struct List{int value;struct List *pointe; }*list; list create_space() {list s(struct List *)malloc(sizeof(struct List)); //向堆区申请空间s->pointe NULL;//初始化s->value 0;return s; } list inserhead_list(lis…

问题排查利器 - 分布式 trace

在分布式系统开发中&#xff0c;系统间的调用往往会横跨多个应用之间的接口。负责的调用链路也导致了&#xff0c;当线上环境出现问题时&#xff0c;例如请求失败、延迟增加或错误发生&#xff0c;我们无法第一时间确定是哪个环节出了问题&#xff0c;这给故障排查和修复带来了…

Spring Data Envers 数据审计实战

随着各行各业信息化发展&#xff0c;决策者们越来越意识到数据版本追踪的重要性&#xff0c;尤其是上市公司&#xff0c;数据对于他们尤为重要。考虑到研发成本&#xff0c;对重要表单数据支持页面级的修改历史查看、对所有业务数据支持DB级的版本查看是一个不错的选择。 对于…

基于OpenCV灰度图像转GCode的斜向扫描实现

基于OpenCV灰度图像转GCode的斜向扫描实现基于OpenCV灰度图像转GCode的斜向扫描实现 引言激光雕刻简介OpenCV简介实现步骤 1.导入必要的库2. 读取灰度图像3. 图像预处理4. 生成GCode5. 保存生成的GCode6. 灰度图像斜向扫描代码示例 总结 系列文章 ⭐深入理解G0和G1指令&…

通过遵循最佳做法来提高 EDA 和 HPC 应用程序的 Azure NetApp 文件性能

介绍 Azure NetApp 文件是一项托管存储解决方案&#xff0c;适用于各种方案&#xff0c;包括高性能计算 (HPC) 基础结构。 低延迟和每秒高 I/O 操作数 (IOPS) 对于大规模企业而言是一种很好的组合。 假设你就职于一家半导体公司。 你的任务是设计公司的集成电路芯片&#xff…

问题:塑瓷后的牙冠要比完成的牙冠大() #学习方法#其他

问题&#xff1a;塑瓷后的牙冠要比完成的牙冠大&#xff08;&#xff09; A.10% B.10%-15% C.15%-20% D.20%-30% E.50% 参考答案如图所示

mysql8通过binlog恢复数据

参考资料: 通过binlog恢复误update的数据(一)_binlog 恢复update-CSDN博客 记录一次工作中的误删除&#xff0c;使用binlog恢复数据的问题。 1&#xff1a;找到mysql8的binlog文件。 2&#xff1a;把binlog文件转换为初始sql文件 mysqlbinlog -vv --base64-outputdecode-row…

【CSS】什么是BFC?BFC有什么作用?

【CSS】什么是BFC&#xff1f;BFC有什么作用&#xff1f; 一、BFC概念二、触发BFC三、BFC特性即应用场景1、解决margin塌陷的问题2、避免外边距margin重叠&#xff08;margin合并&#xff09;3、清除浮动4、阻止元素被浮动元素覆盖 一、BFC概念 BFC(block formatting context)…

【已解决】pt文件转onnx后再转rknn时得到推理图片出现大量锚框变花屏

前言 环境介绍&#xff1a; 1.编译环境 Ubuntu 18.04.5 LTS 2.RKNN版本 py3.8-rknn2-1.4.0 3.单板 迅为itop-3568开发板 一、现象 采用yolov5训练并将pt转换为onnx&#xff0c;再将onnx采用py3.8-rknn2-1.4.0推理转换为rknn&#xff0c;rknn模型能正常转换&#xff0c;…

Mac OS中创建适合网络备份的加密镜像文件:详细步骤与参数选择

这篇文章提供了在Mac OS中创建适合网络备份的加密镜像文件的详细步骤&#xff0c;同时探讨了在选择相关参数时的关键考虑因素&#xff0c;以确保用户能够安全、高效地存储和保护重要数据。 创建步骤 在Mac OS Monterey中&#xff0c;你可以使用“磁盘工具”&#xff08;Disk …

Django通过Json配置文件分配多个定时任务

def load_config():with open("rule.json", rb)as f:config json.load(f)return configdef job(task_name, config, time_interval):# ... 通过task_name判断进行操作if task_name get_data_times:passdef main():config load_config()for task_name, task_value…

代码随想录算法训练营第三十六天|背包问题

01背包问题 二维 代码随想录 视频讲解&#xff1a;带你学透0-1背包问题&#xff01;| 关于背包问题&#xff0c;你不清楚的地方&#xff0c;这里都讲了&#xff01;| 动态规划经典问题 | 数据结构与算法_哔哩哔哩_bilibili public class BagProblem {public static void main(…

LoRA:语言模型微调的计算资源优化策略

编者按&#xff1a;随着数据量和计算能力的增加&#xff0c;大模型的参数量也在不断增加&#xff0c;同时进行大模型微调的成本也变得越来越高。全参数微调需要大量的计算资源和时间&#xff0c;且在进行切换下游任务时代价高昂。 本文作者介绍了一种新方法 LoRA&#xff0c;可…

C++多态_C++回顾

多态的概念 通俗的说多态就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生出不同的概念。 什么是多态 静态的多态 静态的多态即函数重载&#xff0c;编译时是参数匹配和函数名修饰规则。 动态的多态 运行时实现&#xff0c;跟指…

【Java网络编程05】网络原理进阶(三)

1. HTTP协议概述 HTTP协议&#xff1a;又被称为"超文本传输协议"&#xff0c;是一种使用非常广泛的应用层协议&#xff0c;我们之前在文件章节介绍过文本文件与二进制文件的区别&#xff0c;文本可以看做字符串&#xff08;能在utf8/gbk等编码表中查找到合法字符&am…

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

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