zoukankan      html  css  js  c++  java
  • 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) {
    			    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
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
    养成良好习惯,好文章随手顶一下。
  • 相关阅读:
    解决GitHub下载速度太慢的问题
    java监测硬盘空间大小
    @SuppressWarnings注解用法详解
    No goals have been specified for this build.
    java新建excel文件导出(HSSFWorkbook)
    mysql日志文件路径
    获取select框下option所有值
    jquery获取select选中的值
    mysql查看查询缓存是否启用
    Kafka消息重新发送
  • 原文地址:https://www.cnblogs.com/alabo1999/p/15115695.html
Copyright © 2011-2022 走看看