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<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) {
			    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<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);
		}
		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实现数据访问计数器方案详解的文章就介绍到这了,更多相关Spring Boot数据访问计数器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot数据访问自定义使用Druid数据源的方法

    数据访问之Druid数据源的使用 说明:该数据源Druid,使用自定义方式实现,后面文章使用start启动器实现,学习思路为主. 为什么要使用数据源: ​数据源是提高数据库连接性能的常规手段,数据源会负责维持一个数据连接池,当程序创建数据源实例时,系统会一次性地创建多个数据库连接,并把这些数据库连接保存在连接池中. ​当程序需要进行数据库访问时,无须重新获得数据库连接,而是从连接池中取出一个空闲的数据库连接. ​当程序使用数据库连接访问数据库结束后,无须关闭数据库连接,而是将数据库连接归还给连接

  • SpringBoot+MyBatis简单数据访问应用的实例代码

    因为实习用的是MyBatis框架,所以写一篇关于SpringBoot整合MyBatis框架的总结. 一,Pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:

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

    目录 1.数据访问计数器 2.代码实现 2.1.方案说明 2.2.代码 2.3.调用 1.数据访问计数器   在Spring Boot项目中,有时需要数据访问计数器.大致有下列三种情形: 1)纯计数:如登录的密码错误计数,超过门限N次,则表示计数器满,此时可进行下一步处理,如锁定该账户. 2)时间滑动窗口:设窗口宽度为T,如果窗口中尾帧时间与首帧时间差大于T,则表示计数器满.   例如使用redis缓存时,使用key查询redis中数据,如果有此key数据,则返回对象数据:如无此key数据,则查

  • Spring Boot thymeleaf模板引擎的使用详解

    在早期开发的时候,我们完成的都是静态页面也就是html页面,随着时间轴的发展,慢慢的引入了jsp页面,当在后端服务查询到数据之后可以转发到jsp页面,可以轻松的使用jsp页面来实现数据的显示及交互,jsp有非常强大的功能,但是,在使用springboot的时候,整个项目是以jar包的方式运行而不是war包,而且还嵌入了tomcat容器,因此,在默认情况下是不支持jsp页面的.如果直接以纯静态页面的方式会给我们的开发带来很大的麻烦,springboot推荐使用模板引擎. 模板引擎有很多种,jsp,

  • spring boot Slf4j日志框架的体系结构详解

    目录 前言 一.五花八门的日志工具包 1.1. 日志框架 1.2.日志门面 1.3日志门面存在的意义 二.日志框架选型 三.日志级别 四.常见术语概念解析 总结 前言 刚刚接触到java log日志的同学可能会被各种日志框架吓到,包括各种日志框架之间的jar总是发生冲突,另很多小伙伴头疼不已.那我们本篇的内容就是将各种java 日志框架发展过程,以及他们之间的关系,以及如何选型来介绍给大家. 一.五花八门的日志工具包 1.1. 日志框架 JDK java.util.logging 包:java.

  • spring boot中的properties参数配置详解

    application.properties application.properties是spring boot默认的配置文件,spring boot默认会在以下两个路径搜索并加载这个文件 src\main\resources src\main\resources\config 配置系统参数 在application.properties中可配置一些系统参数,spring boot会自动加载这个参数到相应的功能,如下 #端口,默认为8080 server.port=80 #访问路径,默认为/

  • spring boot微服务自定义starter原理详解

    这篇文章主要介绍了spring boot微服务自定义starter原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 使用spring boot开发微服务后,工程的数量大大增加(一定要按照领域来切,不要一个中间件客户端包一个),让各个jar从开发和运行时自包含成了一个重要的内容之一.spring boot starter就可以用来解决该问题(没事启动时别依赖于applicationContext.getBean获取bean进行处理,依赖关系

  • Spring Boot 控制层之参数传递方法详解

    当然,您自己创建一个项目也是可以的. bean包下的Student.java package com.example.demo.bean; public class Student { private Integer id; //学号 private String name; //姓名 public Student() { } public Student(Integer id, String name) { this.id = id; this.name = name; } public In

  • spring boot application properties配置实例代码详解

    废话不多说了,直接给大家贴代码了,具体代码如下所示: # =================================================================== # COMMON SPRING BOOT PROPERTIES # # This sample file is provided as a guideline. Do NOT copy it in its # entirety to your own application. ^^^ # ========

  • Spring boot注解@Async线程池实例详解

    这篇文章主要介绍了Spring boot注解@Async线程池实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 从Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法.调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行. 1. TaskExecutor Spring异步线程池的接口类,其实质是java.util.concurrent

  • Spring Boot读取resources目录文件方法详解

    这篇文章主要介绍了Spring Boot读取resources目录文件方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在Java编码过程中,我们常常希望读取项目内的配置文件,按照Maven的习惯,这些文件一般放在项目的src/main/resources下,因此,合同协议PDF模板.Excel格式的统计报表等模板的存放位置是resources/template/test.pdf,下面提供两种读取方式,它们分别在windows和Linux

  • Spring Boot自定义错误视图的方法详解

    Spring Boot缺省错误视图解析器 Web应用在处理请求的过程中发生错误是非常常见的情况,SpringBoot中为我们实现了一个错误视图解析器(DefaultErrorViewResolver).它基于一些常见的约定,尝试根据HTTP错误状态码解析出错误处理视图.它会在目录/error下针对提供的HTTP错误状态码搜索模板或者静态资源,比如,给定了HTTP状态码404,它会尝试搜索如下模板或者静态资源: /<templates>/error/404.<ext> - 这里<

随机推荐