SpringBoot基于過(guò)濾器和內(nèi)存實(shí)現(xiàn)重復(fù)請(qǐng)求攔截功能
對(duì)于一些請(qǐng)求服務(wù)器的接口,可能存在重復(fù)發(fā)起請(qǐng)求,如果是查詢操作倒是并無(wú)大礙,但是如果涉及到寫(xiě)入操作,一旦重復(fù),可能對(duì)業(yè)務(wù)邏輯造成很?chē)?yán)重的后果,例如交易的接口如果重復(fù)請(qǐng)求可能會(huì)重復(fù)下單。
這里我們使用過(guò)濾器的方式對(duì)進(jìn)入服務(wù)器的請(qǐng)求進(jìn)行過(guò)濾操作,實(shí)現(xiàn)對(duì)相同客戶端請(qǐng)求同一個(gè)接口的過(guò)濾。
@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("請(qǐng)求的MD5值為:{}", hex); if (fastMap.containsKey(hex)) { throw new IllegalStateException("請(qǐng)求重復(fù),請(qǐng)稍后重試!"); } fastMap.put(hex, 10 * 1000L); fastMap.expired(hex, 10 * 1000L, (key, val) -> System.out.println("map:" + fastMap + ",刪除的key:" + key + ",線程名:" + Thread.currentThread().getName())); } log.info("請(qǐng)求的 address:{}", address); chain.doFilter(request, response); } }
通過(guò)繼承Spring中的OncePerRequestFilter過(guò)濾器,確保在一次請(qǐng)求中只通過(guò)一次filter,而不需要重復(fù)的執(zhí)行
通過(guò)獲取請(qǐng)求體中的數(shù)據(jù),計(jì)算出MD5值,存儲(chǔ)在基于內(nèi)存實(shí)現(xiàn)的FastMap中,F(xiàn)astMap的鍵為MD5值,value表示多久以內(nèi)不能重復(fù)請(qǐng)求,這里配置的是10s內(nèi)不能重復(fù)請(qǐng)求。通過(guò)調(diào)用FastMap的expired()
方法,設(shè)置該請(qǐng)求的過(guò)期時(shí)間和過(guò)期時(shí)的回調(diào)函數(shù)
@Component public class FastMap { /** * 按照時(shí)間順序保存了會(huì)過(guò)期key集合,為了實(shí)現(xiàn)快速刪除,結(jié)構(gòu):時(shí)間戳 -> key 列表 */ private final TreeMap<Long, List<String>> expireKeysMap = new TreeMap<>(); /** * 保存會(huì)過(guò)期key的過(guò)期時(shí)間 */ private final Map<String, Long> keyExpireMap = new ConcurrentHashMap<>(); /** * 保存鍵過(guò)期的回調(diào)函數(shù) */ private final HashMap<String, ExpireCallback<String, Long>> keyExpireCallbackMap = new HashMap<>(); private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); /** * 數(shù)據(jù)寫(xiě)鎖 */ private final Lock dataWriteLock = readWriteLock.writeLock(); /** * 數(shù)據(jù)讀鎖 */ private final Lock dataReadLock = readWriteLock.readLock(); private final ReentrantReadWriteLock expireKeysReadWriteLock = new ReentrantReadWriteLock(); /** * 過(guò)期key寫(xiě)鎖 */ private final Lock expireKeysWriteLock = expireKeysReadWriteLock.writeLock(); /** * 過(guò)期key讀鎖 */ private final Lock expireKeysReadLock = expireKeysReadWriteLock.readLock(); /** * 定時(shí)執(zhí)行服務(wù)(全局共享線程池) */ private volatile ScheduledExecutorService scheduledExecutorService; /** * 100萬(wàn),1毫秒=100萬(wàn)納秒 */ private static final int ONE_MILLION = 100_0000; /** * 構(gòu)造器,enableExpire配置是否啟用過(guò)期,不啟用排序 */ public FastMap() { this.init(); } /** * 初始化 */ private void init() { // 雙重校驗(yàn)構(gòu)造一個(gè)單例的scheduledExecutorService if (scheduledExecutorService == null) { synchronized (FastMap.class) { if (scheduledExecutorService == null) { // 啟用定時(shí)器,定時(shí)刪除過(guò)期key,1秒后啟動(dòng),定時(shí)1秒, 因?yàn)闀r(shí)間間隔計(jì)算基于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<String, Long> callback) { // 對(duì)過(guò)期數(shù)據(jù)寫(xiě)上鎖 expireKeysWriteLock.lock(); try { // 使用nanoTime消除系統(tǒng)時(shí)間的影響,轉(zhuǎn)成毫秒存儲(chǔ)降低timeKey數(shù)量,過(guò)期時(shí)間精確到毫秒級(jí)別 Long expireTime = (System.nanoTime() / ONE_MILLION + ms); this.keyExpireMap.put(key, expireTime); List<String> 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) { // 設(shè)置的過(guò)期回調(diào)函數(shù) this.keyExpireCallbackMap.put(key, callback); } // 使用延時(shí)服務(wù)調(diào)用清理key的函數(shù),可以及時(shí)調(diào)用過(guò)期回調(diào)函數(shù) // 同key重復(fù)調(diào)用,會(huì)產(chǎn)生多個(gè)延時(shí)任務(wù),就是多次調(diào)用清理函數(shù),但是不會(huì)產(chǎn)生多次回調(diào),因?yàn)榛卣{(diào)取決于過(guò)期時(shí)間和回調(diào)函數(shù)) scheduledExecutorService.schedule(this::clearExpireData, ms, TimeUnit.MILLISECONDS); ? //假定系統(tǒng)時(shí)間不修改前提下的過(guò)期時(shí)間 return System.currentTimeMillis() + ms; } finally { expireKeysWriteLock.unlock(); } } /** * 清理過(guò)期的數(shù)據(jù) * 調(diào)用時(shí)機(jī):設(shè)置了過(guò)期回調(diào)函數(shù)的key的延時(shí)任務(wù)調(diào)用 */ private void clearExpireData() { // 查找過(guò)期key Long curTimestamp = System.nanoTime() / ONE_MILLION; Map<Long, List<String>> expiredKeysMap = new LinkedHashMap<>(); expireKeysReadLock.lock(); try { // 過(guò)期時(shí)間在【從前至此刻】區(qū)間內(nèi)的都為過(guò)期的key // headMap():獲取從頭到 curTimestamp 元素的集合:不包含 curTimestamp SortedMap<Long, List<String>> sortedMap = this.expireKeysMap.headMap(curTimestamp, true); expiredKeysMap.putAll(sortedMap); } finally { expireKeysReadLock.unlock(); } ? for (Map.Entry<Long, List<String>> entry : expiredKeysMap.entrySet()) { for (String key : entry.getValue()) { // 刪除數(shù)據(jù) Long val = this.remove(key); // 首次調(diào)用刪除(val!=null,前提:val存儲(chǔ)值都不為null) if (val != null) { // 如果存在過(guò)期回調(diào)函數(shù),則執(zhí)行回調(diào) ExpireCallback<String, Long> callback; expireKeysReadLock.lock(); try { callback = this.keyExpireCallbackMap.get(key); } finally { expireKeysReadLock.unlock(); } if (callback != null) { // 回調(diào)函數(shù)創(chuàng)建新線程調(diào)用,防止因?yàn)楹臅r(shí)太久影響線程池的清理工作 // 這里為什么不用線程池調(diào)用,因?yàn)镾cheduledThreadPoolExecutor線程池僅支持核心線程數(shù)設(shè)置,不支持非核心線程的添加 // 核心線程數(shù)用一個(gè)就可以完成清理工作,添加額外的核心線程數(shù)浪費(fèi)了 new Thread(() -> callback.onExpire(key, val), "callback-thread-" + UUID.randomUUID()).start(); } } this.keyExpireCallbackMap.remove(key); } this.expireKeysMap.remove(entry.getKey()); } } }
FastMap通過(guò)ScheduledExecutorService
接口實(shí)現(xiàn)定時(shí)線程任務(wù)的方式對(duì)請(qǐng)求處于過(guò)期時(shí)間的自動(dòng)刪除。
到此這篇關(guān)于SpringBoot基于過(guò)濾器和內(nèi)存實(shí)現(xiàn)重復(fù)請(qǐng)求攔截的文章就介紹到這了,更多相關(guān)SpringBoot重復(fù)請(qǐng)求攔截內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JAVA進(jìn)階之HashMap底層實(shí)現(xiàn)解析
Hashmap是java面試中經(jīng)常遇到的面試題,大部分都會(huì)問(wèn)其底層原理與實(shí)現(xiàn),為了能夠溫故而知新,特地寫(xiě)了這篇文章,以便時(shí)時(shí)學(xué)習(xí)2021-11-11Springboot集成Springbrick實(shí)現(xiàn)動(dòng)態(tài)插件的步驟詳解
這篇文章主要介紹了Springboot集成Springbrick實(shí)現(xiàn)動(dòng)態(tài)插件的詳細(xì)過(guò)程,文中的流程通過(guò)代碼示例介紹的非常詳細(xì),感興趣的同學(xué)可以參考一下2023-06-06Activiti7與Spring以及Spring Boot整合開(kāi)發(fā)
這篇文章主要介紹了Activiti7與Spring以及Spring Boot整合開(kāi)發(fā),在Activiti中核心類的是ProcessEngine流程引擎,與Spring整合就是讓Spring來(lái)管理ProcessEngine,有感興趣的同學(xué)可以參考閱讀2023-03-03java獲取ip地址與網(wǎng)絡(luò)接口的方法示例
這篇文章主要給大家介紹了關(guān)于利用java如何獲取ip地址與網(wǎng)絡(luò)接口的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01Java負(fù)載均衡算法實(shí)現(xiàn)之輪詢和加權(quán)輪詢
網(wǎng)上找了不少負(fù)載均衡算法的資源,都不夠全面,后來(lái)自己結(jié)合了網(wǎng)上的一些算法實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于Java負(fù)載均衡算法實(shí)現(xiàn)之輪詢和加權(quán)輪詢的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04前端發(fā)送的請(qǐng)求Spring如何返回一個(gè)文件詳解
這篇文章主要給大家介紹了關(guān)于前端發(fā)送的請(qǐng)求Spring如何返回一個(gè)文件的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-09-09Java導(dǎo)入導(dǎo)出csv格式文件完整版詳解(附代碼)
在Java中你可以使用不同的庫(kù)來(lái)導(dǎo)出CSV格式的文件,這篇文章主要給大家介紹了關(guān)于Java導(dǎo)入導(dǎo)出csv格式文件的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07基于Java實(shí)現(xiàn)互聯(lián)網(wǎng)實(shí)時(shí)聊天系統(tǒng)(附源碼)
Netty?是一個(gè)利用?Java?的高級(jí)網(wǎng)絡(luò)的能力,隱藏其背后的復(fù)雜性而提供一個(gè)易于使用的?API?的客戶端/服務(wù)器框架。本文將利用它實(shí)現(xiàn)互聯(lián)網(wǎng)實(shí)時(shí)聊天系統(tǒng),感興趣的可以了解一下2022-09-09Spring boot部署發(fā)布到linux的操作方法
這篇文章主要介紹了Spring boot部署發(fā)布到linux的操作方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-05-05