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

作者:阿拉伯1999 时间:2022-12-17 08:40:16 

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/031.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/031.0.0sheng.zheng初版
* 2021/08/081.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 = 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:
* ------------------------------------------------------------------------------
* dateversionmodifierremarks                  
* ------------------------------------------------------------------------------
* 2021/08/031.0.0sheng.zheng初版
* 2021/08/081.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<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:
* ------------------------------------------------------------------------------
* dateversionmodifierremarks                  
* ------------------------------------------------------------------------------
* 2021/08/031.0.0sheng.zheng初版
* 2021/08/081.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);
   // 计数器+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:
* ------------------------------------------------------------------------------
* dateversionmodifierremarks                  
* ------------------------------------------------------------------------------
* 2021/08/031.0.0sheng.zheng初版
* 2021/08/081.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/081.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/081.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/081.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方法:单个数据项不存在时的处理。

  主从关系在数据库中,较为常见,因此针对分组数据和单个对象key分别编写了方法;如果key的个数超过2个,可以类似处理。

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

来源:https://www.cnblogs.com/alabo1999/p/15115695.html

标签:Spring,Boot,数据访问,计数器
0
投稿

猜你喜欢

  • Java Lambda表达式常用的函数式接口

    2021-10-30 13:43:53
  • java连接SQL Server数据库的方法

    2022-10-14 04:16:56
  • WPF在VisualTree上增加Visual

    2023-03-20 00:06:47
  • c#基础知识---委托,匿名函数,lambda

    2023-06-12 18:18:07
  • java 集合工具类Collections及Comparable和Comparator排序详解

    2023-04-29 03:42:06
  • 解析C# 程序结构

    2021-11-15 05:22:59
  • Java数组(Array)最全汇总(中篇)

    2022-09-05 06:01:02
  • SQL Server中的数据复制到的Access中的函数

    2021-10-05 16:06:42
  • cmake跨平台构建工具的学习笔记

    2023-02-04 17:40:14
  • Java内存区域管理详解

    2023-11-10 23:44:42
  • springboot 打包部署 共享依赖包(分布式开发集中式部署微服务)

    2022-12-26 09:13:43
  • Android实现ViewPager无限循环效果(一)

    2022-08-02 18:58:34
  • Java中的SuppressWarnings注解使用

    2023-08-18 17:31:19
  • IDEA最新版2020.1的maven工程本地依赖仓库无法使用问题(已解决)

    2023-09-21 17:57:00
  • Java中的访问修饰符详细解析

    2022-01-18 17:23:02
  • Android 实现单线程轮循机制批量下载图片

    2022-11-05 11:03:45
  • 详解Java编程中JavaMail API的使用

    2022-08-02 06:18:23
  • C#图像处理之头发检测的方法

    2023-11-08 22:10:28
  • C#客户端程序Visual Studio远程调试的方法详解

    2022-10-20 19:13:54
  • Java Fluent Mybatis 项目工程化与常规操作详解流程篇 下

    2021-07-19 11:33:11
  • asp之家 软件编程 m.aspxhome.com