Spring Boot实现数据访问计数器方案详解

在Spring Boot项目中,有时需要数据访问计数器,怎么实现数据访问计数器呢?下面小编给大家带来了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		: * ------------------------------------------------------------------------------ * 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 itemMap; // 对象key的访问滑动窗口 private Map> 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(); }else if (counterType == 2 || counterType == 3) { // 如果与滑动窗口有关 itemSlideWindowMap = new HashMap>(); } } /** * * @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 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 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 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		: * ------------------------------------------------------------------------------ * 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 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		: * ------------------------------------------------------------------------------ * 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个,可以类似处理。

作者:阿拉伯1999 出处:http://www.cnblogs.com/alabo1999/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利. 养成良好习惯,好文章随手顶一下。

以上就是Spring Boot实现数据访问计数器方案详解的详细内容,更多请关注0133技术站其它相关文章!

赞(0) 打赏
未经允许不得转载:0133技术站首页 » Java