buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

利用Hutool-cache来改造本地缓存数据

 

程序中有个从数据字典表获取数据记录的service -- TDicdataService。

§ 利用ScheduledThreadPoolExecutor实现本地数据缓存

考虑到频繁获取字典数据,后来做了本地缓存。实现方案是利用ScheduledThreadPoolExecutor#schedule 。 在频繁访问这个方法过程中,设定每10分钟清理内存数据。

package com.cn.yft.ora.service.impl;

import com.cn.yft.ora.dao.TDicdataDAO;
import com.cn.yft.ora.entity.TDicdata;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TDicdataService {

    private static HashMap<String,List<TDicdata>> cacheByDicmemo = new HashMap<>();
    private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
@Autowired
private TDicdataDAO tDicdataDAO; @SuppressWarnings("unchecked") public List<TDicdata> selectByDicmemo(String dicmemo) {if(StringUtil.isEmpty(dicmemo)) return null; List<TDicdata> list = cacheByDicmemo.get(dicmemo); if(list == null || list.size() == 0){ list = this.tDicdataDAO.selectByDicDesc(dicmemo); cacheByDicmemo.put(dicmemo, list); } if(executor.getActiveCount()+executor.getQueue().size()==0) { executor.schedule(this::cleanLocalCache,10, TimeUnit.MINUTES); } return list; }
// @Scheduled(cron = "0 0/10 * * * ?") 注解@Scheduled对spring无效,需在spring.xml单独配置CronTriggerBean public void cleanLocalCache() { if(!cacheByDicmemo.isEmpty()) { int size=cacheByDicmemo.size(); cacheByDicmemo.clear(); log.info("#字典缓存已清空---清空缓存---本地缓存--已清空本地内存数据{}条", size); } } }

 

§ 利用Hutool-cache来改造本地缓存数据

说来真巧。上面缓存优化是在2020年年底改造的。 而今适逢一年,旧历2021年的年底,使用Hutool-cache来再做一次改造升级。

hutool里有如下几种类型的缓存,均派生自实现了cn.hutool.cache.Cache<K, V>接口的抽象类cn.hutool.cache.impl.AbstractCache<K, V>:

✅ cn.hutool.cache.impl public class FIFOCache<K, V>
FIFO(first in first out) 先进先出缓存
元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)。优点:简单快速 缺点:不灵活,不能保证最常用的对象总是被保留。

✅ cn.hutool.cache.impl public class LFUCache<K, V>
LFU(least frequently used) 最少使用率缓存
根据使用次数来判定对象是否被持续缓存,使用率是通过访问次数计算的。 当缓存满时清理过期对象。 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。

✅ cn.hutool.cache.impl public class LRUCache<K, V>
LRU (least recently used)最近最久未使用缓存
根据使用时间来判定对象是否被持续缓存。当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。 此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。 这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。 缺点是当缓存满时,不能被很快的访问。

✅ cn.hutool.cache.impl public class TimedCache<K, V>
定时缓存 此缓存没有容量限制,对象只有在过期后才会被移除。


针对本文这个场景,我采用最少使用率缓存——LFUCache。

package com.cn.yft.ora.service.impl;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.LFUCache;
import com.cn.yft.ora.dao.TDicdataDAO;
import com.cn.yft.ora.entity.TDicdata;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;

import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TDicdataService {

    // 缓存TTL:10分钟
    private static LFUCache<String,List<TDicdata>> cacheByDicmemo = CacheUtil.newLFUCache(16, TimeUnit.MINUTES.toMillis(10));

    @Autowired
    private TDicdataDAO tDicdataDAO;

    @SuppressWarnings("unchecked")
    public List<TDicdata> selectByDicmemo(String dicmemo) {if(StringUtil.isEmpty(dicmemo)) return null;
        List<TDicdata> list = cacheByDicmemo.get(dicmemo);
        if(list == null || list.size() == 0){
            list = this.tDicdataDAO.selectByDicDesc(dicmemo);
            log.debug("获取key={},结果条数={}", dicmemo, list.size());
            cacheByDicmemo.put(dicmemo, list);
        }
        return list;
    }
@Deprecated // 下面方法不再使用,标记作废
public void cleanLocalCache() { throw new NotImplementedException(); } }

 

§ hutool有点“小糊涂”→说说hutool-cache的Cache#get(K)方法

当使用Hutool缓存库时,需要调用`Cache`接口的`get`方法来获取缓存对象。`Cache`接口的`get`方法具有两个重载版本。下面`cn.hutool.cache.Cache`中的源代码展示了两者的方法签名。

    /**
     * 从缓存中获得对象,当对象不在缓存中或已经过期返回<code>null</code>
     * <p>
     * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回<code>null</code>,否则返回值。
     * <p>
     * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。
     * 
     * @param key 键
     * @return 键对应的对象
     * @see #get(Object, boolean)
     */
    V get(K key);

    /**
     * 从缓存中获得对象,当对象不在缓存中或已经过期返回<code>null</code>
     * <p>
     * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回<code>null</code>,否则返回值。
     * 
     * @param key 键
     * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。
     * @return 键对应的对象
     */
    V get(K key, boolean isUpdateLastAccess);

需要指出的是,默认的get(K)会自动自动刷新缓存ttl,也就是会自动为缓存续命。这对于定时缓存TimedCache,反而不友好。通常我们设置了超时时间,是希望缓存仅在指定的时间内有效,过期则失效。一旦过期不失效,就会出现因数据不一致进而产生的程序bug。

因此,在读取缓存时,最好使用第二个get重载,并将`isUpdateLastAccess`参数设置为`false`。即:使用myCache.get(key, false);来获取缓存对象,同时不更新最近访问时间。

BTW,我把上面这2段文字发给ChatGPT,并告诉它“把这句话用技术语言再润色一下。”。下面是它的回复,很棒。

“请注意,在Hutool缓存库中,默认的`get(K)`方法会自动刷新缓存的TTL(存活时间),从而延长缓存的有效期。然而,对于定时缓存(如`TimedCache`),这个行为可能并不理想,我们通常希望缓存在指定的时间内有效,过期后失效。为了确保预期的行为,建议使用`get`方法的第二个重载版本,并将`isUpdateLastAccess`参数设置为`false`。例如,可以使用`myCache.get(key, false)`来获取缓存对象,同时不更新最近访问时间。”

 

posted on 2022-01-28 18:58  buguge  阅读(3191)  评论(0编辑  收藏  举报