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

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » Spring Boot实现数据访问计数器