当前位置:

首页 > 编程开发 > SpringBoot基于过滤器和内存如何实现重复请求拦截功能

SpringBoot基于过滤器和内存如何实现重复请求拦截功能

对于一些请求服务器的接口,可能存在重复发起请求,如果是查询操作倒是并无大碍,但是如果涉及到写入操作,一旦重复,可能对业务逻辑造成很严重的后果,例如交易的接口如果重复请求可能会重复下单。这里我们使用过滤器的方式对进入服务器的请求进行过滤操作,实现对相同客户端请求同一个接口的过滤。@Slf4j@ComponentpublicclassIRequestFilterextendsOncePerRequestFilter{@ResourceprivateFastMapfastMap;@Overrideprotect

对于一些请求服务器的接口,可能存在重复发起请求,如果是查询操作倒是并无大碍,但是如果涉及到写入操作,一旦重复,可能对业务逻辑造成很严重的后果,例如交易的接口如果重复请求可能会重复下单。

这里我们使用过滤器的方式对进入服务器的请求进行过滤操作,实现对相同客户端请求同一个接口的过滤。

 @Slf4j
 @Component
 public class IRequestFilter extends OncePerRequestFilter {
     @Resource
     private FastMap fastMap;
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
         String address = attributes != null ? attributes.getRequest().getRemoteAddr() : UUID.randomUUID().toString();
         if (Objects.equals(request.getMethod(), "GET")) {
             StringBuilder str = new StringBuilder();
             str.append(request.getRequestURI()).append("|")
                     .append(request.getRemotePort()).append("|")
                     .append(request.getLocalName()).append("|")
                     .append(address);
             String hex = DigestUtil.md5Hex(new String(str));
             log.info("请求的MD5值为:{}", hex);
             if (fastMap.containsKey(hex)) {
                 throw new IllegalStateException("请求重复,请稍后重试!");
             }
             fastMap.put(hex, 10 * 1000L);
             fastMap.expired(hex, 10 * 1000L, (key, val) -> System.out.println("map:" + fastMap + ",删除的key:" + key + ",线程名:" + Thread.currentThread().getName()));
         }
         log.info("请求的 address:{}", address);
         chain.doFilter(request, response);
     }
 }

通过继承Spring中的OncePerRequestFilter过滤器,确保在一次请求中只通过一次filter,而不需要重复的执行

通过获取请求体中的数据,计算出MD5值,存储在基于内存实现的FastMap中,FastMap的键为MD5值,value表示多久以内不能重复请求,这里配置的是10s内不能重复请求。通过调用FastMap的expired()方法,设置该请求的过期时间和过期时的回调函数

 @Component
 public class FastMap {
     /**
      * 按照时间顺序保存了会过期key集合,为了实现快速删除,结构:时间戳 -> key 列表
      */
     private final TreeMap> expireKeysMap = new TreeMap<>();
     /**
      * 保存会过期key的过期时间
      */
     private final Map keyExpireMap = new ConcurrentHashMap<>();
     /**
      * 保存键过期的回调函数
      */
     private final HashMap> keyExpireCallbackMap = new HashMap<>();
     private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
     /**
      * 数据写锁
      */
     private final Lock dataWriteLock = readWriteLock.writeLock();
     /**
      * 数据读锁
      */
     private final Lock dataReadLock = readWriteLock.readLock();
     private final ReentrantReadWriteLock expireKeysReadWriteLock = new ReentrantReadWriteLock();
     /**
      * 过期key写锁
      */
     private final Lock expireKeysWriteLock = expireKeysReadWriteLock.writeLock();
     /**
      * 过期key读锁
      */
     private final Lock expireKeysReadLock = expireKeysReadWriteLock.readLock();
     /**
      * 定时执行服务(全局共享线程池)
      */
     private volatile ScheduledExecutorService scheduledExecutorService;
     /**
      * 100万,1毫秒=100万纳秒
      */
     private static final int ONE_MILLION = 100_0000;
     /**
      * 构造器,enableExpire配置是否启用过期,不启用排序
      */
     public FastMap() {
         this.init();
     }
     /**
      * 初始化
      */
     private void init() {
         // 双重校验构造一个单例的scheduledExecutorService
         if (scheduledExecutorService == null) {
             synchronized (FastMap.class) {
                 if (scheduledExecutorService == null) {
                     // 启用定时器,定时删除过期key,1秒后启动,定时1秒, 因为时间间隔计算基于nanoTime,比timer.schedule更靠谱
                     scheduledExecutorService = new ScheduledThreadPoolExecutor(1, runnable -> {
                         Thread thread = new Thread(runnable, "expireTask-" + UUID.randomUUID());
                         thread.setDaemon(true);
                         return thread;
                     });
                 }
             }
         }
     }
     public boolean containsKey(Object key) {
         dataReadLock.lock();
         try {
             return this.keyExpireMap.containsKey(key);
         } finally {
             dataReadLock.unlock();
         }
     }
     public Long put(String key, Long value) {
         dataWriteLock.lock();
         try {
             return this.keyExpireMap.put(key, value);
         } finally {
             dataWriteLock.unlock();
         }
     }
     public Long remove(Object key) {
         dataWriteLock.lock();
         try {
             return this.keyExpireMap.remove(key);
         } finally {
             dataWriteLock.unlock();
         }
     }
     public Long expired(String key, Long ms, ExpireCallback callback) {
         // 对过期数据写上锁
         expireKeysWriteLock.lock();
         try {
             // 使用nanoTime消除系统时间的影响,转成毫秒存储降低timeKey数量,过期时间精确到毫秒级别
             Long expireTime = (System.nanoTime() / ONE_MILLION + ms);
             this.keyExpireMap.put(key, expireTime);
             List keys = this.expireKeysMap.get(expireTime);
             if (keys == null) {
                 keys = new ArrayList<>();
                 keys.add(key);
                 this.expireKeysMap.put(expireTime, keys);
             } else {
                 keys.add(key);
             }
             if (callback != null) {
                 // 设置的过期回调函数
                 this.keyExpireCallbackMap.put(key, callback);
             }
             // 使用延时服务调用清理key的函数,可以及时调用过期回调函数
             // 同key重复调用,会产生多个延时任务,就是多次调用清理函数,但是不会产生多次回调,因为回调取决于过期时间和回调函数)
             scheduledExecutorService.schedule(this::clearExpireData, ms, TimeUnit.MILLISECONDS);
 
             //假定系统时间不修改前提下的过期时间
             return System.currentTimeMillis() + ms;
         } finally {
             expireKeysWriteLock.unlock();
         }
     }
     /**
      * 清理过期的数据
      * 调用时机:设置了过期回调函数的key的延时任务调用
      */
     private void clearExpireData() {
         // 查找过期key
         Long curTimestamp = System.nanoTime() / ONE_MILLION;
         Map> expiredKeysMap = new LinkedHashMap<>();
         expireKeysReadLock.lock();
         try {
             // 过期时间在【从前至此刻】区间内的都为过期的key
             // headMap():获取从头到 curTimestamp 元素的集合:不包含 curTimestamp
             SortedMap> sortedMap = this.expireKeysMap.headMap(curTimestamp, true);
             expiredKeysMap.putAll(sortedMap);
         } finally {
             expireKeysReadLock.unlock();
         }
 
         for (Map.Entry> entry : expiredKeysMap.entrySet()) {
             for (String key : entry.getValue()) {
                 // 删除数据
                 Long val = this.remove(key);
                 // 首次调用删除(val!=null,前提:val存储值都不为null)
                 if (val != null) {
                     // 如果存在过期回调函数,则执行回调
                     ExpireCallback callback;
                     expireKeysReadLock.lock();
                     try {
                         callback = this.keyExpireCallbackMap.get(key);
                     } finally {
                         expireKeysReadLock.unlock();
                     }
                     if (callback != null) {
                         // 回调函数创建新线程调用,防止因为耗时太久影响线程池的清理工作
                         // 这里为什么不用线程池调用,因为ScheduledThreadPoolExecutor线程池仅支持核心线程数设置,不支持非核心线程的添加
                         // 核心线程数用一个就可以完成清理工作,添加额外的核心线程数浪费了
                         new Thread(() -> callback.onExpire(key, val), "callback-thread-" + UUID.randomUUID()).start();
                     }
                 }
                 this.keyExpireCallbackMap.remove(key);
             }
             this.expireKeysMap.remove(entry.getKey());
         }
     }
 }

FastMap通过ScheduledExecutorService接口实现定时线程任务的方式对请求处于过期时间的自动删除。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。