SpringBoot基于過濾器和內(nèi)存實現(xiàn)重復(fù)請求攔截功能
對于一些請求服務(wù)器的接口,可能存在重復(fù)發(fā)起請求,如果是查詢操作倒是并無大礙,但是如果涉及到寫入操作,一旦重復(fù),可能對業(yè)務(wù)邏輯造成很嚴重的后果,例如交易的接口如果重復(fù)請求可能會重復(fù)下單。
這里我們使用過濾器的方式對進入服務(wù)器的請求進行過濾操作,實現(xiàn)對相同客戶端請求同一個接口的過濾。
@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("請求重復(fù),請稍后重試!");
}
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,而不需要重復(fù)的執(zhí)行
通過獲取請求體中的數(shù)據(jù),計算出MD5值,存儲在基于內(nèi)存實現(xiàn)的FastMap中,F(xiàn)astMap的鍵為MD5值,value表示多久以內(nèi)不能重復(fù)請求,這里配置的是10s內(nèi)不能重復(fù)請求。通過調(diào)用FastMap的expired()方法,設(shè)置該請求的過期時間和過期時的回調(diào)函數(shù)
@Component
public class FastMap {
/**
* 按照時間順序保存了會過期key集合,為了實現(xiàn)快速刪除,結(jié)構(gòu):時間戳 -> key 列表
*/
private final TreeMap<Long, List<String>> expireKeysMap = new TreeMap<>();
/**
* 保存會過期key的過期時間
*/
private final Map<String, Long> keyExpireMap = new ConcurrentHashMap<>();
/**
* 保存鍵過期的回調(diào)函數(shù)
*/
private final HashMap<String, ExpireCallback<String, Long>> keyExpireCallbackMap = new HashMap<>();
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 數(shù)據(jù)寫鎖
*/
private final Lock dataWriteLock = readWriteLock.writeLock();
/**
* 數(shù)據(jù)讀鎖
*/
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();
/**
* 定時執(zhí)行服務(wù)(全局共享線程池)
*/
private volatile ScheduledExecutorService scheduledExecutorService;
/**
* 100萬,1毫秒=100萬納秒
*/
private static final int ONE_MILLION = 100_0000;
/**
* 構(gòu)造器,enableExpire配置是否啟用過期,不啟用排序
*/
public FastMap() {
this.init();
}
/**
* 初始化
*/
private void init() {
// 雙重校驗構(gòu)造一個單例的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) {
// 對過期數(shù)據(jù)寫上鎖
expireKeysWriteLock.lock();
try {
// 使用nanoTime消除系統(tǒng)時間的影響,轉(zhuǎn)成毫秒存儲降低timeKey數(shù)量,過期時間精確到毫秒級別
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è)置的過期回調(diào)函數(shù)
this.keyExpireCallbackMap.put(key, callback);
}
// 使用延時服務(wù)調(diào)用清理key的函數(shù),可以及時調(diào)用過期回調(diào)函數(shù)
// 同key重復(fù)調(diào)用,會產(chǎn)生多個延時任務(wù),就是多次調(diào)用清理函數(shù),但是不會產(chǎn)生多次回調(diào),因為回調(diào)取決于過期時間和回調(diào)函數(shù))
scheduledExecutorService.schedule(this::clearExpireData, ms, TimeUnit.MILLISECONDS);
?
//假定系統(tǒng)時間不修改前提下的過期時間
return System.currentTimeMillis() + ms;
} finally {
expireKeysWriteLock.unlock();
}
}
/**
* 清理過期的數(shù)據(jù)
* 調(diào)用時機:設(shè)置了過期回調(diào)函數(shù)的key的延時任務(wù)調(diào)用
*/
private void clearExpireData() {
// 查找過期key
Long curTimestamp = System.nanoTime() / ONE_MILLION;
Map<Long, List<String>> expiredKeysMap = new LinkedHashMap<>();
expireKeysReadLock.lock();
try {
// 過期時間在【從前至此刻】區(qū)間內(nèi)的都為過期的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存儲值都不為null)
if (val != null) {
// 如果存在過期回調(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)用,防止因為耗時太久影響線程池的清理工作
// 這里為什么不用線程池調(diào)用,因為ScheduledThreadPoolExecutor線程池僅支持核心線程數(shù)設(shè)置,不支持非核心線程的添加
// 核心線程數(shù)用一個就可以完成清理工作,添加額外的核心線程數(shù)浪費了
new Thread(() -> callback.onExpire(key, val), "callback-thread-" + UUID.randomUUID()).start();
}
}
this.keyExpireCallbackMap.remove(key);
}
this.expireKeysMap.remove(entry.getKey());
}
}
}FastMap通過ScheduledExecutorService接口實現(xiàn)定時線程任務(wù)的方式對請求處于過期時間的自動刪除。
到此這篇關(guān)于SpringBoot基于過濾器和內(nèi)存實現(xiàn)重復(fù)請求攔截的文章就介紹到這了,更多相關(guān)SpringBoot重復(fù)請求攔截內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot如何返回Json數(shù)據(jù)格式
這篇文章主要介紹了SpringBoot如何返回Json數(shù)據(jù)格式問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Java中Double除保留后小數(shù)位的幾種方法(小結(jié))
這篇文章主要介紹了Java中Double保留后小數(shù)位的幾種方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07
Java實現(xiàn)PDF轉(zhuǎn)為Word文檔的示例代碼
眾所周知,PDF文檔除了具有較強穩(wěn)定性和兼容性外,?還具有較強的安全性,在工作中可以有效避免別人無意中對文檔內(nèi)容進行修改。本文將分為以下兩部分介紹如何在保持布局的情況下將PDF轉(zhuǎn)為Word文檔,希望對大家有所幫助2023-01-01
SpringBoot多環(huán)境配置及配置文件分類實例詳解
這篇文章主要介紹了SpringBoot多環(huán)境配置及配置文件分類,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-10-10
MyBatis嵌套查詢collection報錯:org.apache.ibatis.exceptions.TooMany
本文主要介紹了MyBatis嵌套查詢collection報錯:org.apache.ibatis.exceptions.TooManyResultsException,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-09-09
Java 使用Axis調(diào)用WebService的示例代碼
這篇文章主要介紹了Java 使用Axis調(diào)用WebService的示例代碼,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-09-09
詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法
本文主要介紹了詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04

