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、代码实现

1)使用字典来管理不同的key,因为不同的key需要单独计数。

2)上述三种情况,使用类型属性区分,并在构造函数中进行设置。

3)滑动窗口使用双向队列Deque来实现。

4)考虑到访问并发性,读取或更新时,加锁保护。

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 itemMap;

    // 对象key的访问滑动窗口
    private Map> 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();
        }else if (counterType == 2 || counterType == 3) {
            // 如果与滑动窗口有关
            itemSlideWindowMap = new HashMap>();
        }
    }

    /**
     *
     * @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 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 itemQueue = itemSlideWindowMap.get(itemKey);
            synchronized(itemQueue) {
                Long head = 0L;
                // 循环处理头部数据,确保新数据帧加入后,维持窗口宽度
                while(true) {
                    // 取得头部数据
                    head = itemQueue.peekFirst();
                    if (head == null || timeMillis - head = 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 itemQueue = itemSlideWindowMap.get(itemKey);
            if (itemQueue.size() > 0) {
                // 加锁保护
                synchronized(itemQueue) {
                  // 清空
                  itemQueue.clear();
                }
            }
            }
        }else if(this.counterType == 3){
            // 如果为滑窗+数量类型
            if (itemSlideWindowMap.containsKey(itemKey)) {
                Deque 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 itemQueue = itemSlideWindowMap.get(itemKey);
            // 加锁保护
            synchronized(itemQueue) {
                // 加入
                itemQueue.add(timeMillis);
            }
            }else {
            // 新key值,加锁保护
            Deque itemQueue = new ArrayDeque();
            synchronized(itemSlideWindowMap) {
                // 加入映射表
                itemSlideWindowMap.put(itemKey, itemQueue);
                itemQueue.add(timeMillis);
            }
            }
        }else if(this.counterType == 3){
            // 如果为滑窗+数量类型
            if (itemSlideWindowMap.containsKey(itemKey)) {
                Deque itemQueue = itemSlideWindowMap.get(itemKey);
            // 加锁保护
            synchronized(itemQueue) {
                Long head = 0L;
                // 循环处理头部数据
                while(true) {
                    // 取得头部数据
                head = itemQueue.peekFirst();
                if (head == null || timeMillis - head  itemQueue = new ArrayDeque();
            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();
            }
        }
    }
}

要调用计数器,只需在应用类中添加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个,可以类似处理。

Original: https://www.cnblogs.com/alabo1999/p/15115695.html
Author: 阿拉伯1999
Title: Spring Boot实现数据访问计数器

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/610018/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球