Spring Boot实现数据访问计数器

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: * ------------------------------------------------------------------------------ * dateversionmodifierremarks* ------------------------------------------------------------------------------ * 2021/08/03 1.0.0sheng.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:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/03 1.0.0sheng.zheng初版* 2021/08/08 1.0.1sheng.zheng支持多种类型计数器**/ public boolean isItemKeyFull(String itemKey,Long timeMillis) {boolean bRet = false;if (this.counterType == 1) {// 如果为计数器类型if (itemMap.containsKey(itemKey)) {synchronized(itemMap) {Integer value = https://tazarkount.com/read/itemMap.get(itemKey);// 如果计数器将超越门限if (value >= this.counterThreshold - 1) {bRet = true;}}}else {// 新的对象key , 视业务需要 , 取值true或falsebRet = true;}}else if(this.counterType == 2){// 如果为滑窗类型if (itemSlideWindowMap.containsKey(itemKey)) {Deque itemQueue = itemSlideWindowMap.get(itemKey);synchronized(itemQueue) {if (itemQueue.size() > 0) {Long head = itemQueue.getFirst();if (timeMillis - head >= this.windowSize) {// 如果窗口将满bRet = true;}}}}else {// 新的对象key , 视业务需要 , 取值true或falsebRet = true;}}else if(this.counterType == 3){// 如果为滑窗+数量类型if (itemSlideWindowMap.containsKey(itemKey)) {Deque 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或falsebRet = true;}}return bRet;}/**** @methodName: resetItemKey* @description: 复位对象key的计数* @param itemKey : 对象key* @history:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/03 1.0.0sheng.zheng初版* 2021/08/08 1.0.1sheng.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 itemQueue = itemSlideWindowMap.get(itemKey);if (itemQueue.size() > 0) {// 加锁保护synchronized(itemQueue) {// 清空itemQueue.clear();}}}}else if(this.counterType == 3){// 如果为滑窗+数量类型if (itemSlideWindowMap.containsKey(itemKey)) {Deque itemQueue = itemSlideWindowMap.get(itemKey);synchronized(itemQueue) {// 清空itemQueue.clear();}}} }/**** @methodName: putItemkey* @description: 更新对象key的计数* @param itemKey : 对象key* @param timeMillis: 时间戳 , 毫秒数 , 如为滑窗类计数器 , 使用此参数值* @history:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/03 1.0.0sheng.zheng初版* 2021/08/08 1.0.1sheng.zheng支持多种类型计数器**/ public void putItemkey(String itemKey,Long timeMillis) {if (this.counterType == 1) {// 如果为计数器类型if (itemMap.containsKey(itemKey)) {// 更新值 , 加锁保护synchronized(itemMap) {Integer value = itemMap.get(itemKey);// 计数器+1value ++;itemMap.put(itemKey, value);}}else {// 新key值 , 加锁保护synchronized(itemMap) {itemMap.put(itemKey, 1);}}}else if(this.counterType == 2){// 如果为滑窗类型if (itemSlideWindowMap.containsKey(itemKey)) {Deque itemQueue = itemSlideWindowMap.get(itemKey);// 加锁保护synchronized(itemQueue) {// 加入itemQueue.add(timeMillis);}}else {// 新key值 , 加锁保护Deque itemQueue = new ArrayDeque();synchronized(itemSlideWindowMap) {// 加入映射表itemSlideWindowMap.put(itemKey, itemQueue);itemQueue.add(timeMillis);}}}else if(this.counterType == 3){// 如果为滑窗+数量类型if (itemSlideWindowMap.containsKey(itemKey)) {Deque 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 itemQueue = new ArrayDeque();synchronized(itemSlideWindowMap) {// 加入映射表itemSlideWindowMap.put(itemKey, itemQueue);itemQueue.add(timeMillis);}}}}/**** @methodName : clear* @description : 清空字典* @history:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/03 1.0.0sheng.zheng初版* 2021/08/08 1.0.1sheng.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:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/08 1.0.0sheng.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:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/08 1.0.0sheng.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:* ------------------------------------------------------------------------------* dateversionmodifierremarks* ------------------------------------------------------------------------------* 2021/08/08 1.0.0sheng.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方法:单个数据项不存在时的处理 。
【Spring Boot实现数据访问计数器】??主从关系在数据库中 , 较为常见 , 因此针对分组数据和单个对象key分别编写了方法;如果key的个数超过2个 , 可以类似处理 。
作者:阿拉伯1999出处:http://www.cnblogs.com/alabo1999/本文版权归作者和博客园共有 , 欢迎转载 , 但未经作者同意必须保留此段声明 , 且在文章页面明显位置给出原文连接 , 否则保留追究法律责任的权利.养成良好习惯 , 好文章随手顶一下 。