Spring Boot實(shí)現(xiàn)數(shù)據(jù)訪問計(jì)數(shù)器方案詳解
1、數(shù)據(jù)訪問計(jì)數(shù)器
在Spring Boot項(xiàng)目中,有時(shí)需要數(shù)據(jù)訪問計(jì)數(shù)器。大致有下列三種情形:
1)純計(jì)數(shù):如登錄的密碼錯(cuò)誤計(jì)數(shù),超過門限N次,則表示計(jì)數(shù)器滿,此時(shí)可進(jìn)行下一步處理,如鎖定該賬戶。
2)時(shí)間滑動(dòng)窗口:設(shè)窗口寬度為T,如果窗口中尾幀時(shí)間與首幀時(shí)間差大于T,則表示計(jì)數(shù)器滿。
例如使用redis緩存時(shí),使用key查詢r(jià)edis中數(shù)據(jù),如果有此key數(shù)據(jù),則返回對(duì)象數(shù)據(jù);如無此key數(shù)據(jù),則查詢數(shù)據(jù)庫,但如果一直都無此key數(shù)據(jù),從而反復(fù)查詢數(shù)據(jù)庫,顯然有問題。此時(shí),可使用時(shí)間滑動(dòng)窗口,對(duì)于查詢的失敗的key,距離首幀T時(shí)間(如1分鐘)內(nèi),不再查詢數(shù)據(jù)庫,而是直接返回?zé)o此數(shù)據(jù),直到新查詢的時(shí)間超過T,更新滑窗首幀為新時(shí)間,并執(zhí)行一次查詢數(shù)據(jù)庫操作。
3)時(shí)間滑動(dòng)窗口+計(jì)數(shù):這往往在需要進(jìn)行限流處理的場(chǎng)景使用。如T時(shí)間(如1分鐘)內(nèi),相同key的訪問次數(shù)超過超過門限N,則表示計(jì)數(shù)器滿,此時(shí)進(jìn)行限流處理。
2、代碼實(shí)現(xiàn)
2.1、方案說明
1)使用字典來管理不同的key,因?yàn)椴煌膋ey需要單獨(dú)計(jì)數(shù)。
2)上述三種情況,使用類型屬性區(qū)分,并在構(gòu)造函數(shù)中進(jìn)行設(shè)置。
3)滑動(dòng)窗口使用雙向隊(duì)列Deque來實(shí)現(xiàn)。
4)考慮到訪問并發(fā)性,讀取或更新時(shí),加鎖保護(hù)。
2.2、代碼
package com.abc.example.service; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.Map; /** * @className : DacService * @description : 數(shù)據(jù)訪問計(jì)數(shù)服務(wù)類 * @summary : * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/03 1.0.0 sheng.zheng 初版 * */ public class DacService { // 計(jì)數(shù)器類型:1-數(shù)量;2-時(shí)間窗口;3-時(shí)間窗口+數(shù)量 private int counterType; // 計(jì)數(shù)器數(shù)量門限 private int counterThreshold = 5; // 時(shí)間窗口長度,單位毫秒 private int windowSize = 60000; // 對(duì)象key的訪問計(jì)數(shù)器 private Map<String,Integer> itemMap; // 對(duì)象key的訪問滑動(dòng)窗口 private Map<String,Deque<Long>> itemSlideWindowMap; /** * 構(gòu)造函數(shù) * @param counterType : 計(jì)數(shù)器類型,值為1,2,3之一 * @param counterThreshold : 計(jì)數(shù)器數(shù)量門限,如果類型為1或3,需要此值 * @param windowSize : 窗口時(shí)間長度,如果為類型為2,3,需要此值 */ public DacService(int counterType, int counterThreshold, int windowSize) { this.counterType = counterType; this.counterThreshold = counterThreshold; this.windowSize = windowSize; if (counterType == 1) { // 如果與計(jì)數(shù)器有關(guān) itemMap = new HashMap<String,Integer>(); }else if (counterType == 2 || counterType == 3) { // 如果與滑動(dòng)窗口有關(guān) itemSlideWindowMap = new HashMap<String,Deque<Long>>(); } } /** * * @methodName : isItemKeyFull * @description : 對(duì)象key的計(jì)數(shù)是否將滿 * @param itemKey : 對(duì)象key * @param timeMillis : 時(shí)間戳,毫秒數(shù),如為滑窗類計(jì)數(shù)器,使用此參數(shù)值 * @return : 滿返回true,否則返回false * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/03 1.0.0 sheng.zheng 初版 * 2021/08/08 1.0.1 sheng.zheng 支持多種類型計(jì)數(shù)器 * */ public boolean isItemKeyFull(String itemKey,Long timeMillis) { boolean bRet = false; if (this.counterType == 1) { // 如果為計(jì)數(shù)器類型 if (itemMap.containsKey(itemKey)) { synchronized(itemMap) { Integer value = itemMap.get(itemKey); // 如果計(jì)數(shù)器將超越門限 if (value >= this.counterThreshold - 1) { bRet = true; } } }else { // 新的對(duì)象key,視業(yè)務(wù)需要,取值true或false bRet = true; } }else if(this.counterType == 2){ // 如果為滑窗類型 if (itemSlideWindowMap.containsKey(itemKey)) { Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey); synchronized(itemQueue) { if (itemQueue.size() > 0) { Long head = itemQueue.getFirst(); if (timeMillis - head >= this.windowSize) { // 如果窗口將滿 bRet = true; } } } }else { // 新的對(duì)象key,視業(yè)務(wù)需要,取值true或false bRet = true; } }else if(this.counterType == 3){ // 如果為滑窗+數(shù)量類型 if (itemSlideWindowMap.containsKey(itemKey)) { Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey); synchronized(itemQueue) { Long head = 0L; // 循環(huán)處理頭部數(shù)據(jù),確保新數(shù)據(jù)幀加入后,維持窗口寬度 while(true) { // 取得頭部數(shù)據(jù) head = itemQueue.peekFirst(); if (head == null || timeMillis - head <= this.windowSize) { break; } // 移除頭部 itemQueue.remove(); } if (itemQueue.size() >= this.counterThreshold -1) { // 如果窗口數(shù)量將滿 bRet = true; } } }else { // 新的對(duì)象key,視業(yè)務(wù)需要,取值true或false bRet = true; } } return bRet; } /** * * @methodName : resetItemKey * @description : 復(fù)位對(duì)象key的計(jì)數(shù) * @param itemKey : 對(duì)象key * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/03 1.0.0 sheng.zheng 初版 * 2021/08/08 1.0.1 sheng.zheng 支持多種類型計(jì)數(shù)器 * */ public void resetItemKey(String itemKey) { if (this.counterType == 1) { // 如果為計(jì)數(shù)器類型 if (itemMap.containsKey(itemKey)) { // 更新值,加鎖保護(hù) synchronized(itemMap) { itemMap.put(itemKey, 0); } } }else if(this.counterType == 2){ // 如果為滑窗類型 // 清空 if (itemSlideWindowMap.containsKey(itemKey)) { Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey); if (itemQueue.size() > 0) { // 加鎖保護(hù) synchronized(itemQueue) { // 清空 itemQueue.clear(); } } } }else if(this.counterType == 3){ // 如果為滑窗+數(shù)量類型 if (itemSlideWindowMap.containsKey(itemKey)) { Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey); synchronized(itemQueue) { // 清空 itemQueue.clear(); } } } } /** * * @methodName : putItemkey * @description : 更新對(duì)象key的計(jì)數(shù) * @param itemKey : 對(duì)象key * @param timeMillis : 時(shí)間戳,毫秒數(shù),如為滑窗類計(jì)數(shù)器,使用此參數(shù)值 * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/03 1.0.0 sheng.zheng 初版 * 2021/08/08 1.0.1 sheng.zheng 支持多種類型計(jì)數(shù)器 * */ public void putItemkey(String itemKey,Long timeMillis) { if (this.counterType == 1) { // 如果為計(jì)數(shù)器類型 if (itemMap.containsKey(itemKey)) { // 更新值,加鎖保護(hù) synchronized(itemMap) { Integer value = itemMap.get(itemKey); // 計(jì)數(shù)器+1 value ++; itemMap.put(itemKey, value); } }else { // 新key值,加鎖保護(hù) synchronized(itemMap) { itemMap.put(itemKey, 1); } } }else if(this.counterType == 2){ // 如果為滑窗類型 if (itemSlideWindowMap.containsKey(itemKey)) { Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey); // 加鎖保護(hù) synchronized(itemQueue) { // 加入 itemQueue.add(timeMillis); } }else { // 新key值,加鎖保護(hù) Deque<Long> itemQueue = new ArrayDeque<Long>(); synchronized(itemSlideWindowMap) { // 加入映射表 itemSlideWindowMap.put(itemKey, itemQueue); itemQueue.add(timeMillis); } } }else if(this.counterType == 3){ // 如果為滑窗+數(shù)量類型 if (itemSlideWindowMap.containsKey(itemKey)) { Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey); // 加鎖保護(hù) synchronized(itemQueue) { Long head = 0L; // 循環(huán)處理頭部數(shù)據(jù) while(true) { // 取得頭部數(shù)據(jù) head = itemQueue.peekFirst(); if (head == null || timeMillis - head <= this.windowSize) { break; } // 移除頭部 itemQueue.remove(); } // 加入新數(shù)據(jù) itemQueue.add(timeMillis); } }else { // 新key值,加鎖保護(hù) Deque<Long> itemQueue = new ArrayDeque<Long>(); synchronized(itemSlideWindowMap) { // 加入映射表 itemSlideWindowMap.put(itemKey, itemQueue); itemQueue.add(timeMillis); } } } } /** * * @methodName : clear * @description : 清空字典 * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/03 1.0.0 sheng.zheng 初版 * 2021/08/08 1.0.1 sheng.zheng 支持多種類型計(jì)數(shù)器 * */ public void clear() { if (this.counterType == 1) { // 如果為計(jì)數(shù)器類型 synchronized(this) { itemMap.clear(); } }else if(this.counterType == 2){ // 如果為滑窗類型 synchronized(this) { itemSlideWindowMap.clear(); } }else if(this.counterType == 3){ // 如果為滑窗+數(shù)量類型 synchronized(this) { itemSlideWindowMap.clear(); } } } }
2.3、調(diào)用
要調(diào)用計(jì)數(shù)器,只需在應(yīng)用類中添加DacService對(duì)象,如:
public class DataCommonService { // 數(shù)據(jù)訪問計(jì)數(shù)服務(wù)類,時(shí)間滑動(dòng)窗口,窗口寬度60秒 protected DacService dacService = new DacService(2,0,60000); /** * * @methodName : procNoClassData * @description : 對(duì)象組key對(duì)應(yīng)的數(shù)據(jù)不存在時(shí)的處理 * @param classKey : 對(duì)象組key * @return : 數(shù)據(jù)加載成功,返回true,否則為false * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/08 1.0.0 sheng.zheng 初版 * */ protected boolean procNoClassData(Object classKey) { boolean bRet = false; String key = getCombineKey(null,classKey); Long currentTime = System.currentTimeMillis(); // 判斷計(jì)數(shù)器是否將滿 if (dacService.isItemKeyFull(key,currentTime)) { // 如果計(jì)數(shù)將滿 // 復(fù)位 dacService.resetItemKey(key); // 從數(shù)據(jù)庫加載分組數(shù)據(jù)項(xiàng) bRet = loadGroupItems(classKey); } dacService.putItemkey(key,currentTime); return bRet; } /** * * @methodName : procNoItemData * @description : 對(duì)象key對(duì)應(yīng)的數(shù)據(jù)不存在時(shí)的處理 * @param itemKey : 對(duì)象key * @param classKey : 對(duì)象組key * @return : 數(shù)據(jù)加載成功,返回true,否則為false * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/08 1.0.0 sheng.zheng 初版 * */ protected boolean procNoItemData(Object itemKey, Object classKey) { // 如果itemKey不存在 boolean bRet = false; String key = getCombineKey(itemKey,classKey); Long currentTime = System.currentTimeMillis(); if (dacService.isItemKeyFull(key,currentTime)) { // 如果計(jì)數(shù)將滿 // 復(fù)位 dacService.resetItemKey(key); // 從數(shù)據(jù)庫加載數(shù)據(jù)項(xiàng) bRet = loadItem(itemKey, classKey); } dacService.putItemkey(key,currentTime); return bRet; } /** * * @methodName : getCombineKey * @description : 獲取組合key值 * @param itemKey : 對(duì)象key * @param classKey : 對(duì)象組key * @return : 組合key * @history : * ------------------------------------------------------------------------------ * date version modifier remarks * ------------------------------------------------------------------------------ * 2021/08/08 1.0.0 sheng.zheng 初版 * */ protected String getCombineKey(Object itemKey, Object classKey) { String sItemKey = (itemKey == null ? "" : itemKey.toString()); String sClassKey = (classKey == null ? "" : classKey.toString()); String key = ""; if (!sClassKey.isEmpty()) { key = sClassKey; } if (!sItemKey.isEmpty()) { if (!key.isEmpty()) { key += "-" + sItemKey; }else { key = sItemKey; } } return key; } }
procNoClassData方法:分組數(shù)據(jù)不存在時(shí)的處理。procNoItemData方法:單個(gè)數(shù)據(jù)項(xiàng)不存在時(shí)的處理。
主從關(guān)系在數(shù)據(jù)庫中,較為常見,因此針對(duì)分組數(shù)據(jù)和單個(gè)對(duì)象key分別編寫了方法;如果key的個(gè)數(shù)超過2個(gè),可以類似處理。
作者:阿拉伯1999 出處:http://www.cnblogs.com/alabo1999/ 本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利. 養(yǎng)成良好習(xí)慣,好文章隨手頂一下。
到此這篇關(guān)于Spring Boot實(shí)現(xiàn)數(shù)據(jù)訪問計(jì)數(shù)器方案詳解的文章就介紹到這了,更多相關(guān)Spring Boot數(shù)據(jù)訪問計(jì)數(shù)器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA項(xiàng)目如何取消git版本管控并添加svn版本控制
在公司內(nèi)部服務(wù)器環(huán)境下,將代碼倉庫從Gitee的Git遷移到SVN可以避免外部版本控制的風(fēng)險(xiǎn),遷移過程中,先刪除項(xiàng)目的.git文件夾,再通過Eclipse的設(shè)置界面刪除原Git配置并添加SVN配置,之后,將項(xiàng)目提交到SVN倉庫,確保使用ignore列表過濾不必要的文件2024-10-10解決java.sql.SQLException:索引中丟失 IN或OUT 參數(shù)::x問題
這篇文章主要介紹了解決java.sql.SQLException:索引中丟失 IN或OUT 參數(shù)::x問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12idea創(chuàng)建SpringBoot項(xiàng)目時(shí)Type選maven?project和maven?pom有何區(qū)別
Maven是一個(gè)Java工程的管理工具,跟其相同功能的工具如Gradle,下面這篇文章主要給大家介紹了關(guān)于idea創(chuàng)建SpringBoot項(xiàng)目時(shí)Type選maven?project和maven?pom有何區(qū)別的相關(guān)資料,需要的朋友可以參考下2023-02-02Java實(shí)現(xiàn)多線程下載和斷點(diǎn)續(xù)傳
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)多線程下載和斷點(diǎn)續(xù)傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06