您好,登錄后才能下訂單哦!
這篇文章主要介紹了SpringBoot基于過濾器和內存如何實現重復請求攔截功能的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇SpringBoot基于過濾器和內存如何實現重復請求攔截功能文章都會有所收獲,下面我們一起來看看吧。
對于一些請求服務器的接口,可能存在重復發起請求,如果是查詢操作倒是并無大礙,但是如果涉及到寫入操作,一旦重復,可能對業務邏輯造成很嚴重的后果,例如交易的接口如果重復請求可能會重復下單。
這里我們使用過濾器的方式對進入服務器的請求進行過濾操作,實現對相同客戶端請求同一個接口的過濾。
@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<Long, List<String>> expireKeysMap = new TreeMap<>(); /** * 保存會過期key的過期時間 */ private final Map<String, Long> keyExpireMap = new ConcurrentHashMap<>(); /** * 保存鍵過期的回調函數 */ private final HashMap<String, ExpireCallback<String, Long>> 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<String, Long> callback) { // 對過期數據寫上鎖 expireKeysWriteLock.lock(); try { // 使用nanoTime消除系統時間的影響,轉成毫秒存儲降低timeKey數量,過期時間精確到毫秒級別 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) { // 設置的過期回調函數 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<Long, List<String>> expiredKeysMap = new LinkedHashMap<>(); expireKeysReadLock.lock(); try { // 過期時間在【從前至此刻】區間內的都為過期的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()) { // 刪除數據 Long val = this.remove(key); // 首次調用刪除(val!=null,前提:val存儲值都不為null) if (val != null) { // 如果存在過期回調函數,則執行回調 ExpireCallback<String, Long> 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
接口實現定時線程任務的方式對請求處于過期時間的自動刪除。
關于“SpringBoot基于過濾器和內存如何實現重復請求攔截功能”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“SpringBoot基于過濾器和內存如何實現重復請求攔截功能”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。