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) {
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);
}else {
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);
}else {
// 计数不满
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个,可以类似处理。

Spring Boot实现数据访问计数器的更多相关文章

  1. Spring Boot的数据访问:CrudRepository接口的使用

    示例 使用CrudRepository接口访问数据 创建一个新的Maven项目,命名为crudrepositorytest.按照Maven项目的规范,在src/main/下新建一个名为resource ...

  2. (8)Spring Boot 与数据访问

    文章目录 简介 整合基本的JDBC与数据源 整合 druid 数据源 整合 mybatis 简介 对于数据访问层,无论是 SQL 还是 NOSQL ,Spring Boot 默认都采用整合 Sprin ...

  3. Spring Boot的数据访问 之Spring Boot + jpa的demo

    1. 快速地创建一个项目,pom中选择如下 <?xml version="1.0" encoding="UTF-8"?> <project x ...

  4. Spring Boot框架 - 数据访问 - 整合Mybatis

    一.新建Spring Boot项目 注意:创建的时候勾选Mybatis依赖,pom文件如下 <dependency> <groupId>org.mybatis.spring.b ...

  5. Spring Boot框架 - 数据访问 - JDBC&自动配置

    一.新建Spring Boot 工程 特殊勾选数据库相关两个依赖 Mysql Driver — 数据库驱动 Spring Data JDBC 二.配置文件application.properties ...

  6. Spring MVC或Spring Boot配置默认访问页面不生效?

    相信在开发项目过程中,设置默认访问页面应该都用过.但是有时候设置了却不起作用.你知道是什么原因吗?今天就来说说我遇到的问题. 首先说说配置默认访问页面有哪几种方式. 1.tomcat配置默认访问页面 ...

  7. Spring boot未授权访问造成的数据库外联

    一.spring boot 日常测试或攻防演练中像shiro,fastjson等漏洞已经越来越少了,但是随着spring boot框架的广泛使用,spring boot带来的安全问题也越来越多,本文仅 ...

  8. Spring Boot与数据

    SpringBoot 着眼于JavaEE! 不仅仅局限于 Mybatis .JDBC. Spring Data JPA Spring Data 项目的目的是为了简化构建基于 Spring 框架应用的数 ...

  9. Spring boot通过JPA访问MySQL数据库

    本文展示如何通过JPA访问MySQL数据库. JPA全称Java Persistence API,即Java持久化API,它为Java开发人员提供了一种对象/关系映射工具来管理Java应用中的关系数据 ...

随机推荐

  1. Vue(4)Vue指令的学习1

    前言 Vue官网一共有提供了14个指令,分别如下 v-text v-html v-show v-if ☆☆☆ v-else ☆☆☆ v-else-if ☆☆☆ v-for ☆☆☆ v-on ☆☆☆ v ...

  2. 通过AI识图判断图片是否为小票

    先在百度智能云中创建一个应用加入以下标记功能(没有智能云账号可以去创建一个,创建应用也都是些基本操作) 本次只用到标记的功能. 此功能在图像识别下面. 创建应用后,页面会出现平台分配的密钥:API K ...

  3. mysql的主从复制延迟问题--看这一篇就够了

    ​ 在之前我们已经讲解了一主一从,双主双从的mysql集群搭建,在单机应用的时候看起来没有问题,但是在企业的生产环境中,在很多情况下都会有复制延迟的问题. ​ 主从复制的原理我们在此处就不再赘述了,之 ...

  4. js笔记22

    1.在拖拽元素的时候,如果元素的内部加了文字或者图片,拖拽效果会失灵? 浏览器会给文字和图片一个默认行为,当文字和图片被选中的时候,会有一个拖拽的效果,即使我们没有人为给他添加.所以当我们点击这个元素 ...

  5. Docker与k8s的恩怨情仇(四)-云原生时代的闭源落幕

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 在本系列前几篇文章中,我们介绍了从Cloud Foundry到Docker等PaaS平台的发展迭代过程.今天 ...

  6. C# 启动项目直接修改电脑的日期格式

    启动界面添加 1.引用添加 using System.Runtime.InteropServices; 2.方法添加 [DllImport("kernel32.dll", Entr ...

  7. Raspberry Pi:树莓派开发板配置USB启动系统

    准备材料 树莓派4B U盘 TF卡 树莓派基础镜像2020-08-20稳定版(这个系统是必须的并拷录在TF卡) Kali树莓派系统(这个是我想要学习的系统,大家可以准备自己的系统,拷录在U盘的) SD ...

  8. JavaWeb中Servlet和Jsp跳转路径的写法

    最近学习时,常常要写一些页面之间的跳转或者前台和后端之间的跳转 下面总结一下自己对于这些跳转路径的写法 声明:以下讲到的jsp文件都默认在WebRoot目录下 1.表单(Jsp)->Servle ...

  9. String、StringBuilder和StringBuffer的比较

    目录 1.String特性 1.1 不可变 1.2 字符串常量池 2.StringBuilder和StringBuffer 2.1 区别 2.2 应用场景 1.String特性 1.1 不可变 它是I ...

  10. 重置networker8.0密码

    一.重置Networker 8.0密码 1.设置环境变量 新建一个"系统环境变量"名字为"GST_RESET_PW",值为1 2.重启EMC GST Servi ...