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

网友投稿 261 2022-10-07


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

目录1、数据访问计数器2、代码实现2.1、方案说明2.2、代码2.3、调用

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();

http:// 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();

} http://

}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个,可以类似处理。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:用过滤器防sql注入(防止SQL注入的最佳方式)
下一篇:31类API安全测试的小技巧(接口的安全测试)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~