1、資料訪問計數器
在Spring Boot專案中,有時需要資料訪問計數器。大致有下列三種情形:
1)純計數:如登入的密碼錯誤計數,超過門限N次,則表示計數器滿,此時可進行下一步處理,如鎖定該賬戶。
2)時間滑動視窗:設視窗寬度為T,如果視窗中尾幀時間與首幀時間差大於T,則表示計數器滿。
例如使用redis快取時,使用key查詢redis中資料,如果有此key資料,則返回物件資料;如無此key資料,則查詢資料庫,但如果一直都無此key資料,從而反覆查詢資料庫,顯然有問題。此時,可使用時間滑動視窗,對於查詢的失敗的key,距離首幀T時間(如1分鐘)內,不再查詢資料庫,而是直接返回無此資料,直到新查詢的時間超過T,更新滑窗首幀為新時間,並執行一次查詢資料庫操作。
3)時間滑動視窗+計數:這往往在需要進行限流處理的場景使用。如T時間(如1分鐘)內,相同key的訪問次數超過超過門限N,則表示計數器滿,此時進行限流處理。
2、程式碼實現
2.1、方案說明
1)使用字典來管理不同的key,因為不同的key需要單獨計數。
2)上述三種情況,使用型別屬性區分,並在建構函式中進行設定。
3)滑動視窗使用雙向佇列Deque來實現。
4)考慮到訪問併發性,讀取或更新時,加鎖保護。
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 : 資料訪問計數服務類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/03 1.0.0 sheng.zheng 初版
*
*/
public class DacService {
// 計數器型別:1-數量;2-時間視窗;3-時間視窗+數量
private int counterType;
// 計數器數量門限
private int counterThreshold = 5;
// 時間視窗長度,單位毫秒
private int windowSize = 60000;
// 物件key的訪問計數器
private Map<String,Integer> itemMap;
// 物件key的訪問滑動視窗
private Map<String,Deque<Long>> itemSlideWindowMap;
/**
* 建構函式
* @param counterType : 計數器型別,值為1,2,3之一
* @param counterThreshold : 計數器數量門限,如果型別為1或3,需要此值
* @param windowSize : 視窗時間長度,如果為型別為2,3,需要此值
*/
public DacService(int counterType, int counterThreshold, int windowSize) {
this.counterType = counterType;
this.counterThreshold = counterThreshold;
this.windowSize = windowSize;
if (counterType == 1) {
// 如果與計數器有關
itemMap = new HashMap<String,Integer>();
}else if (counterType == 2 || counterType == 3) {
// 如果與滑動視窗有關
itemSlideWindowMap = new HashMap<String,Deque<Long>>();
}
}
/**
*
* @methodName : isItemKeyFull
* @description : 物件key的計數是否將滿
* @param itemKey : 物件key
* @param timeMillis : 時間戳,毫秒數,如為滑窗類計數器,使用此引數值
* @return : 滿返回true,否則返回false
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/03 1.0.0 sheng.zheng 初版
* 2021/08/08 1.0.1 sheng.zheng 支援多種型別計數器
*
*/
public boolean isItemKeyFull(String itemKey,Long timeMillis) {
boolean bRet = false;
if (this.counterType == 1) {
// 如果為計數器型別
if (itemMap.containsKey(itemKey)) {
synchronized(itemMap) {
Integer value = itemMap.get(itemKey);
// 如果計數器將超越門限
if (value >= this.counterThreshold - 1) {
bRet = true;
}
}
}else {
// 新的物件key,視業務需要,取值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 {
// 新的物件key,視業務需要,取值true或false
bRet = true;
}
}else if(this.counterType == 3){
// 如果為滑窗+數量型別
if (itemSlideWindowMap.containsKey(itemKey)) {
Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey);
synchronized(itemQueue) {
Long head = 0L;
// 迴圈處理頭部資料,確保新資料幀加入後,維持視窗寬度
while(true) {
// 取得頭部資料
head = itemQueue.peekFirst();
if (head == null || timeMillis - head <= this.windowSize) {
break;
}
// 移除頭部
itemQueue.remove();
}
if (itemQueue.size() >= this.counterThreshold -1) {
// 如果視窗數量將滿
bRet = true;
}
}
}else {
// 新的物件key,視業務需要,取值true或false
bRet = true;
}
}
return bRet;
}
/**
*
* @methodName : resetItemKey
* @description : 復位物件key的計數
* @param itemKey : 物件key
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/03 1.0.0 sheng.zheng 初版
* 2021/08/08 1.0.1 sheng.zheng 支援多種型別計數器
*
*/
public void resetItemKey(String itemKey) {
if (this.counterType == 1) {
// 如果為計數器型別
if (itemMap.containsKey(itemKey)) {
// 更新值,加鎖保護
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) {
// 加鎖保護
synchronized(itemQueue) {
// 清空
itemQueue.clear();
}
}
}
}else if(this.counterType == 3){
// 如果為滑窗+數量型別
if (itemSlideWindowMap.containsKey(itemKey)) {
Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey);
synchronized(itemQueue) {
// 清空
itemQueue.clear();
}
}
}
}
/**
*
* @methodName : putItemkey
* @description : 更新物件key的計數
* @param itemKey : 物件key
* @param timeMillis : 時間戳,毫秒數,如為滑窗類計數器,使用此引數值
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/03 1.0.0 sheng.zheng 初版
* 2021/08/08 1.0.1 sheng.zheng 支援多種型別計數器
*
*/
public void putItemkey(String itemKey,Long timeMillis) {
if (this.counterType == 1) {
// 如果為計數器型別
if (itemMap.containsKey(itemKey)) {
// 更新值,加鎖保護
synchronized(itemMap) {
Integer value = itemMap.get(itemKey);
// 計數器+1
value ++;
itemMap.put(itemKey, value);
}
}else {
// 新key值,加鎖保護
synchronized(itemMap) {
itemMap.put(itemKey, 1);
}
}
}else if(this.counterType == 2){
// 如果為滑窗型別
if (itemSlideWindowMap.containsKey(itemKey)) {
Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey);
// 加鎖保護
synchronized(itemQueue) {
// 加入
itemQueue.add(timeMillis);
}
}else {
// 新key值,加鎖保護
Deque<Long> itemQueue = new ArrayDeque<Long>();
synchronized(itemSlideWindowMap) {
// 加入對映表
itemSlideWindowMap.put(itemKey, itemQueue);
itemQueue.add(timeMillis);
}
}
}else if(this.counterType == 3){
// 如果為滑窗+數量型別
if (itemSlideWindowMap.containsKey(itemKey)) {
Deque<Long> itemQueue = itemSlideWindowMap.get(itemKey);
// 加鎖保護
synchronized(itemQueue) {
Long head = 0L;
// 迴圈處理頭部資料
while(true) {
// 取得頭部資料
head = itemQueue.peekFirst();
if (head == null || timeMillis - head <= this.windowSize) {
break;
}
// 移除頭部
itemQueue.remove();
}
// 加入新資料
itemQueue.add(timeMillis);
}
}else {
// 新key值,加鎖保護
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 支援多種型別計數器
*
*/
public void clear() {
if (this.counterType == 1) {
// 如果為計數器型別
synchronized(this) {
itemMap.clear();
}
}else if(this.counterType == 2){
// 如果為滑窗型別
synchronized(this) {
itemSlideWindowMap.clear();
}
}else if(this.counterType == 3){
// 如果為滑窗+數量型別
synchronized(this) {
itemSlideWindowMap.clear();
}
}
}
}
2.3、呼叫
要呼叫計數器,只需在應用類中新增DacService物件,如:
public class DataCommonService {
// 資料訪問計數服務類,時間滑動視窗,視窗寬度60秒
protected DacService dacService = new DacService(2,0,60000);
/**
*
* @methodName : procNoClassData
* @description : 物件組key對應的資料不存在時的處理
* @param classKey : 物件組key
* @return : 資料載入成功,返回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();
// 判斷計數器是否將滿
if (dacService.isItemKeyFull(key,currentTime)) {
// 如果計數將滿
// 復位
dacService.resetItemKey(key);
// 從資料庫載入分組資料項
bRet = loadGroupItems(classKey);
}
dacService.putItemkey(key,currentTime);
return bRet;
}
/**
*
* @methodName : procNoItemData
* @description : 物件key對應的資料不存在時的處理
* @param itemKey : 物件key
* @param classKey : 物件組key
* @return : 資料載入成功,返回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)) {
// 如果計數將滿
// 復位
dacService.resetItemKey(key);
// 從資料庫載入資料項
bRet = loadItem(itemKey, classKey);
}
dacService.putItemkey(key,currentTime);
return bRet;
}
/**
*
* @methodName : getCombineKey
* @description : 獲取組合key值
* @param itemKey : 物件key
* @param classKey : 物件組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方法:分組資料不存在時的處理。procNoItemData方法:單個資料項不存在時的處理。
主從關係在資料庫中,較為常見,因此針對分組資料和單個物件key分別編寫了方法;如果key的個數超過2個,可以類似處理。