使用拦截器+Redis实现接口幂等

news2025/1/21 6:00:09

文章目录

  • 使用拦截器+Redis实现接口幂等
    • 1.思路分析
    • 2.具体实现
      • 2.1 创建redis工具类
      • 2.2 自定义幂等注解
      • 2.2 自定义幂等拦截器
      • 2.3 注入拦截器到容器
    • 3.测试

使用拦截器+Redis实现接口幂等

1.思路分析

接口幂等有很多种实现方式,拦截器/AOP+Redis,拦截器/AOP+本地缓存等等,本文讲解一下通过拦截器+Redis实现幂等的方式。

其原理就是在拦截器中拦截请求,然后根据一个标识符去redis中查询是否已经存在,如果存在,则说明当前请求正在处理,抛出异常告诉前端请勿重复请求。

标识符:一般可以使用token+methodType+uri作为标识符,具体业务具体分析。

2.具体实现

2.1 创建redis工具类

import com.yunling.sys.config.exception.ParamValidateException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * Redis工具类
 *
 * @author 谭永强
 * @date 2023-08-15
 */
@Component
public class RedisUtils {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 写入缓存
     *
     * @param key   建
     * @param value 值
     * @return 成功/失败
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<String, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 写入缓存设置时效时间
     *
     * @param key   键
     * @param value 值
     * @return 成功/失败
     */
    public boolean set(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<String, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 判断缓存中是否有对应的value
     *
     * @param key 键
     * @return 成功/失败
     */
    public boolean exists(final String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 读取缓存
     *
     * @param key 键
     * @return 成功/失败
     */
    public Object get(final String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 删除对应的value
     *
     * @param key 键
     * @return 成功/失败
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            return Boolean.TRUE.equals(redisTemplate.delete(key));
        }
        return false;
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return 结果
     */
    public Long incr(String key, long delta) {
        if (ObjectUtils.isEmpty(key)) {
            throw new ParamValidateException("key值不能为空");
        }
        if (delta < 0) {
            throw new ParamValidateException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return 结果
     */
    public Long decr(String key, long delta) {
        if (ObjectUtils.isEmpty(key)) {
            throw new ParamValidateException("key值不能为空");
        }
        if (delta < 0) {
            throw new ParamValidateException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
}

2.2 自定义幂等注解

自定义幂等注解,将seconds设置为该注解的属性,在拦截器中判断方法上是否有该注解,如果有该注解,则说明当前方法需要做幂等校验。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自动幂等
 * 该注解加在需要幂等的方法上,即可自动上线方法的幂等。
 *
 * @author 谭永强
 * @date 2023-08-15
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

    /**
     * 限定时间(秒)
     * 限制多少秒内,每个用户只能请求一次该接口。
     */
    long seconds() default 1;
}

2.2 自定义幂等拦截器

定义幂等接口用于拦截处理请求。

import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.yunling.sys.annotate.AutoIdempotent;
import com.yunling.sys.common.RedisUtils;
import com.yunling.sys.common.ResultData;
import com.yunling.sys.common.ReturnCode;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * 自动幂等拦截器
 *
 * @author 谭永强
 * @date 2023-08-15
 */
@Component
public class AutoIdempotentInterceptor extends HandlerInterceptorAdapter {

    @Resource
    private RedisUtils redisUtils;

    /**
     * @param request  请求
     * @param response 响应
     * @param handler  处理
     * @return 结果
     * @throws Exception 异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断请求是否为方法的请求
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod method = (HandlerMethod) handler;
        //获取方法中是否有幂等性注解
        AutoIdempotent anno = method.getMethodAnnotation(AutoIdempotent.class);
        //若注解为空则直接返回
        if (Objects.isNull(anno)) {
            return true;
        }
        //限定时间
        long seconds = anno.seconds();
        //token
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (Objects.isNull(token)) {
            ResultData<String> resultData = ResultData.fail(ReturnCode.ACCESS_DENIED.getCode(), "token不能为空");
            write(response, JSON.toJSONString(resultData));
            return false;
        }
        //此处转MD5的原因就是token长度太长了,转成md5短一些,此操作并不是必须的
        String md5 = MD5Utils.md5Hex(token, StandardCharsets.UTF_8.toString());
        //使用token+method+uri作为key值,此处需要通过key值确定请求的唯一性,也可以使用其他的组合,只要保证唯一性即可
        String key = md5 + ":" + request.getMethod() + ":" + request.getRequestURI();
        Object requestCountObj = redisUtils.get(key);
        if (!ObjectUtils.isEmpty(requestCountObj)) {
            //不为空,说明不是第一次请求,直接报错
            ResultData<String> resultData = ResultData.fail(ReturnCode.RC206.getCode(), "请求已提交,请勿重复请求");
            write(response, JSON.toJSONString(resultData));
            return false;
        }
        //若为空则为第一次请求
        return redisUtils.set(key, 1, seconds);
    }

    /**
     * 返回结果到前端
     *
     * @param response 响应
     * @param body     结果
     * @throws IOException 异常
     */
    private void write(HttpServletResponse response, String body) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ServletOutputStream os = response.getOutputStream();
        os.write(body.getBytes());
        os.flush();
        os.close();
    }
}

2.3 注入拦截器到容器

将拦截器注册到容器中。

package com.yunling.sys.config;

import com.yunling.sys.config.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * 将拦截器注入到容器中
 *
 * @author 谭永强
 * @date 2023-08-15
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
    }
}

3.测试

@RestController
@RequestMapping("user")
public class SysUserController {
    
  
    /**
     * 用户新增
     *
     * @param user 用户信息
     */
    @AutoIdempotent(seconds = 60)
    @PostMapping("add")
    public void add(@RequestBody SysUser user) {
       //业务代码.....
    }
}

请求该接口,如果在60s内再次请求,就会返回重复请求的结果。seconds具体值设置多少由该接口的实际响应时间为标准,默认值为1秒。
在这里插入图片描述

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

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

相关文章

以对话为场景本质,AIGC 将如何改变游戏规则

8 月 17 日&#xff08;本周四&#xff09;&#xff0c;融云直播课从排查问题到预警风险&#xff0c;社交产品如何更好保障体验、留住用户&#xff1f;欢迎点击报名~ 生成式 AI 公司 MosaicML 以约 13 亿美元的价格被大数据巨头 Databricks 收购&#xff0c;这个发生于 6 月底的…

python获取音乐文件

浏览器打开音乐地址 http://www.htqyy.com/top/hot 点击第一首歌曲&#xff0c;会打开新的网页并且可以获取 改歌曲的id&#xff0c;就是url中的33 在播放页面点击F12&#xff0c;打开开发者调试功能 如下图所示&#xff0c;在script脚本中可以获取歌曲的下载数据 host&#…

【DICOM医学影像1】数据格式存储于显示,基本知识科普指南

DICOM&#xff08;Digital Imaging and Communications in Medicine&#xff09;数据格式&#xff0c;是医学影像存储中的标准格式。无论是X光、CT&#xff0c;还是MRI等等影像&#xff0c;采集的原理不同&#xff0c;但是存储的格式一般都是统一的。本文就对DICOM文件的图像显…

Hands on RL 之 Deep Deterministic Policy Gradient(DDPG)

Hands on RL 之 Deep Deterministic Policy Gradient&#xff08;DDPG&#xff09; 文章目录 Hands on RL 之 Deep Deterministic Policy Gradient&#xff08;DDPG&#xff09;1. 理论部分1.1 回顾 Deterministic Policy Gradient(DPG)1.2 Neural Network Difference1.3 Why i…

大模型PEFT技术原理(三):Adapter Tuning及其变体

随着预训练模型的参数越来越大&#xff0c;尤其是175B参数大小的GPT3发布以来&#xff0c;让很多中小公司和个人研究员对于大模型的全量微调望而却步&#xff0c;近年来研究者们提出了各种各样的参数高效迁移学习方法&#xff08;Parameter-efficient Transfer Learning&#x…

java17新特性+ZGC

ZGC垃圾收集 11引入的追求低延迟的垃圾回收器 1.ZGC的内存布局 1.1 region 和G1一样&#xff0c;也是基于Region的堆内存布局。但是ZGC的Region具有动态性&#xff1a;动态创建、动态销毁、动态数据容量。 1.2 垃圾回收机制 相较于CMS&#xff0c;ZGC只有6个阶段&#xff1…

同步、异步、协程

目录 同步异步https 异步请求&#xff1a; 协程1.为什么会要协程?2.异步的运行流程是什么3.协程的原语操作4.协程的定义?5.调度器的定义?6.调度的策略?7. api封装, hook8.多核的模式?9.协程的性能?10.要有哪些案例?nty_servernty_ mysql_client.cnty_ mysql oper.cnty_ …

Python项目实战:基于napari的3D可视化(点云+slice)

文章目录 一、napari 简介二、napari 安装与更新三、napari【巨巨巨大的一个BUG】四、napari 使用指南4.1、菜单栏&#xff08;File View Plugins Window Help&#xff09;4.2、Window&#xff1a;layer list&#xff08;参数详解&#xff09;4.3、Window&#xff1a;layer…

city walk结合VR全景,打造新时代下的智慧城市

近期爆火的city walk是什么梗&#xff1f;它其实是近年来备受追捧的城市漫步方式&#xff0c;一种全新的城市探索方式&#xff0c;与传统的旅游观光不同&#xff0c;城市漫步更注重与城市的亲密接触&#xff0c;一步步地感受城市的脉动。其实也是一种自由、休闲的方式&#xff…

vscode搭建java开发环境

一、配置extensions环境变量VSCODE_EXTENSIONS&#xff0c; 该环境变量路径下的存放安装组件&#xff1a; 二、setting配置文件 {"java.jdt.ls.java.home": "e:\\software\\jdk\\jdk17",// java运行环境"java.configuration.runtimes": [{"…

分类预测 | MATLAB实现DRN深度残差网络多输入分类预测

分类预测 | MATLAB实现DRN深度残差网络多输入分类预测 目录 分类预测 | MATLAB实现DRN深度残差网络多输入分类预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.分类预测 | MATLAB实现DRN深度残差网络多输入分类预测 2.代码说明&#xff1a;MATLAB实现DRN深度残差网络…

信捷 XDH Ethercat A_GearIn指令与轴配置

在前面的文章中描述了A_FOLLOW指令&#xff0c;有时不能满足要求&#xff0c;需要更高级的指令A_GearIn指令。 下面的例子A_GearIn指令和CNT_AB指令 实现手轮动马达动&#xff0c;手轮停马达停&#xff0c;手轮转的快马达也转得快。&#xff08;手轮输出接到PLC的X0和X1点&am…

【内测】百度AI搜索体验

收到百度搜索AI体验邀请&#xff0c;简单测试了一下&#xff0c;目前支持文案创作&#xff0c;AI绘画等。 文案创作功能还行&#xff0c;绘画功能效果比较差。

【数据库】P4 过滤数据 WHERE

过滤数据 WHERE 简介WHERE 子句操作符检测单个值案例范围值检查 BETWEEN AND空值检查 NULL 简介 数据库表一般包含大量的数据&#xff0c;很少需要检索表中的所有行。我们只检索所需数据需要指定搜索条件(search criteria)&#xff0c;搜索条件也称为过滤条件(filter conditio…

【每日一题】617. 合并二叉树

【每日一题】617. 合并二叉树 617. 合并二叉树题目描述解题思路 617. 合并二叉树 题目描述 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&#xff0c;两棵树上的一些节点将会重叠&#xff08;而另一些不会&#xff…

SpringBoot携带Jre绿色部署项目

文章目录 SpringBoot携带Jre绿色部署运行项目1. 实现步骤2. 自测项目文件目录及bat文件内容&#xff0c;截图如下&#xff1a;2-1 项目文件夹列表&#xff1a;2-2. bat内容 3. 扩展&#xff1a; 1.6-1.8版本的jdk下载 SpringBoot携带Jre绿色部署运行项目 说明&#xff1a; 实…

【数据结构与算法】十大经典排序算法-归并排序

&#x1f31f;个人博客&#xff1a;www.hellocode.top &#x1f3f0;Java知识导航&#xff1a;Java-Navigate &#x1f525;CSDN&#xff1a;HelloCode. &#x1f31e;知乎&#xff1a;HelloCode &#x1f334;掘金&#xff1a;HelloCode ⚡如有问题&#xff0c;欢迎指正&#…

基本变量与引用变量的区别

基本数据类型创建的变量&#xff0c;称为基本变量&#xff0c;该变量空间中直接存放的是其所对应的值&#xff1b; 而引用数据类型创建的变量&#xff0c;一般称为对象的引用&#xff0c;其空间中存储的是对象所在空间的地址。 public static void func() { int a 10; int b …

vue3 如果切换角色后权限不同 怎么清空之前添加动态路由。

项目中切换角色后发现会保留前面一个角色的权限&#xff0c;方法一是到login页面&#xff0c;权限重新reload&#xff0c;不过这样确实会影响体验&#xff0c;如果不采用此方案的话&#xff0c;你可以看下我从发现问题到解决问题的思路&#xff1a; 1、首先要找到一个初始状态…