# 扩展Hutool缓存销毁策略的一些见解

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Hutool缓存销毁策略扩展

theme: vue-pro

highlight: atom-one-dark-reasonable

先来看看业务场景

首先咱们来思考一下为什么会有Top热榜这个功能,这个业务给搜索带来了哪些好处,想想如果是我们该怎么实现Top榜单这个业务?

假如,我们现在做的是一个搜索相关的项目,在项目初期数据量寥寥无几。一方面可以手动导入数据,但是这样并不能非常精准的将用户所需要的数据给入库,如果有一个地方,我们可以看到用户搜索的内容,那么按需入库这样问题不就迎刃而解了嘛!(说的那么容易,你到底是来写呀)相信你一定使用过Spring-AOP动态代理技术吧。哪像这样不就是一个环绕切面,存入数据库吗?

于是乎,一号选手就位,开始coding。扒拉扒拉的一顿猛敲,什么install、update,噼里啪啦,最终把上面说提及到的AOP写完。emmmm...经过一段比较长的时间用户访问量不断增加数据库终于不堪重负,歇菜...好家伙,这月绩效奖都要被扣完了。

二号选手看见一号选手写的什么辣鸡代码,把库给搞炸了,于是便有了缓存操作。咸蛋少扯,啊呸你以为我是那个咸蛋超人那

下面直接贴代码教他怎么做人

Hutool缓存工具类TimedCache

我这里主要提供一种通用的思路,你可以通过该思路来完成如上所说的业务。当然你可以把他看成一个轻量级的缓存计数器实现,以及在框架不能满足特定要求的情况下,怎么利用继承重写的方式去扩展框架之外不能完成的事情。说了那么多,还不把你代码贴出来,渣男!渣男!渣男!哈呸...

程序的入口-Spring-AOP

/**
 * 搜索日志Aop
 *
 * @author youyongkun
 * @date 2021/5/17 14:47
 */
@Aspect
@Component
public class SearchLogAspect {

    /**
     * 日志缓存
     *
     * @author youyongkun
     * @date 2021/5/17 14:55
     */
    @Autowired
    private SearchLogTimedCache searchLogTimedCache;

    /**
     * 选择题搜索切点
     */
    @Pointcut("execution(* cn.akwangl.service.question.IChoiceQuestionService.match(..))")
    public void questionSearch() {
    }

    /**
     * 4.方法执行前后调用
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("questionSearch()")
    public Object doBeforeAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取目标方法的参数信息
        Object[] args = joinPoint.getArgs();
        String key = (String) args[0];
        SearchLog searchLog = searchLogTimedCache.get(key);
        if (searchLog == null) {
            searchLog = new SearchLog();
            searchLog.setKeyWorld(key);
            searchLog.setSearchNum(1L);
            searchLogTimedCache.put(key, searchLog);
        } else {
            searchLog.setSearchNum(searchLog.getSearchNum() + 1);
        }
        Object result = joinPoint.proceed(args);

        if (result != null && ((ArrayList) result).size() > 0) {
            searchLog.setIsHit(YesOrNO.YES.getType());
        } else {
            searchLog.setIsHit(YesOrNO.NO.getType());
        }
        return result;
    }
}

SearchLogAspect 类主要在questionSearch切点进行业务keyworld关键字记数以及根据返回接口来标记搜索是否命中。在这个SearchLogAspect中有一个searchLogTimedCache成员变量,今天就是基于hutool#TimedCache这个类来实现前端用户搜索框keyworld记数

为什么一定要用TimedCache来完成这个业务

首先,通过Spring-AOP直接访问数据库是可以完成计数功能。但是这样频繁读写数据库,而业务本身只是一个类似计数器的功能,像这样频繁读写数据库完全没必要,我们可以把100次的写入操作变成一次,来减少数据库压力。那么使用缓存来做计数,那问题又来了,那JVM缓存写满了怎么办?正如TimedCache这个类名字,我们可以设定在什么时候自动的让缓存的数据入库并清除缓存。

感兴趣的小伙伴可以 cn.hutool.cache.impl.TimedCache查看这个类的源码,代码的主程序是在 cn.hutool.cache.impl.AbstractCache抽象类中。

首先我们来分析一下TimedCache什么时候会清除缓存

TimedCache的父类(cn.hutool.cache.impl.AbstractCache)中可以看到有一个抽象方法,这个抽象方法的实现就是用来清除缓存数据,他的调用在cn.hutool.cache.impl.AbstractCache#prune方法中,该方法是线程安全的

/**
 * 清理实现<br>
 * 子类实现此方法时无需加锁
 *
 * @return 清理数
 */
protected abstract int pruneCache();

怎样才能在清除的时候将数据入库呢?

找了找cn.hutool.cache.impl.TimedCache#pruneCache()实现,并没有看到有相关的回调函数,那么我们只能自己继承cn.hutool.cache.impl.TimedCache类,重写pruneCache()方法加入自己的逻辑

根据Java方法重载的就近原则,当有两个 pruneCache()一样的方法时,优先使用自己的

于是乎代码就是下面这个样子

/**
 * 对hutool 缓存清除功能扩展
 *
 * @author youyongkun
 * @date 2021/5/16 9:49 下午
 */
@Slf4j
public abstract class AbstractTimedCache<K, V> extends cn.hutool.cache.impl.TimedCache<K, V> {
    /**
     * 默认过期时间 30分钟
     */
    protected static long DEFAULT_TIMEOUT = (60 * 1000) * 30;

    public AbstractTimedCache() {
        super((long) ((DEFAULT_TIMEOUT * 0.7) + DEFAULT_TIMEOUT));
    }

    public AbstractTimedCache(long timeout) {
        super(timeout);
    }

    public AbstractTimedCache(long timeout, Map map) {
        super(timeout, map);
    }

    /**
     * 保存日志信息
     *
     * @param logsMap 日志集合
     * @return true:成功 false:失败
     * @author youyongkun
     * @date 2021/5/17 13:56
     */
    protected abstract boolean save(Map<K, V> logsMap);

    @Override
    protected int pruneCache() {
        Map<K, V> logsMap = new HashMap();
        int count = 0;
        Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
        CacheObj<K, V> co;
        while (values.hasNext()) {
            co = values.next();
            try {
                Method isExpired = co.getClass().getDeclaredMethod("isExpired");
                isExpired.setAccessible(true);
                boolean flag = (boolean) isExpired.invoke(co, null);
                if (flag) {
                    logsMap.put(co.getKey(), co.getValue());
                    values.remove();
                    onRemove(co.getKey(), co.getValue());
                    count++;
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 回调入库
        if (logsMap.size() > 0) {
            boolean saveFlag = save(logsMap);
            if (saveFlag) {
                log.debug("缓存日志入库成功,受影响{}条数,完整数据{}", logsMap.size(), logsMap);
            } else {
                log.debug("缓存日志入库失败,完整数据{}", logsMap);
            }
        }
        return count;
    }
}

在这里我并没有把入库的业务逻辑直接写入到pruneCache()方法中,而是在pruneCache()添加了一个save()的抽象方法,这样做的目的是提高代码的复用性。这里需要注意的是在判断CacheObj是否过期时,cn.hutool.cache.impl.TimedCache#pruneCache()源码中是直接使用cn.hutool.cache.impl.CacheObj#isExpired()方法进行判断,而这个方法的访问权限是默认的,因此我使用Java 反射来调用cn.hutool.cache.impl.CacheObj#isExpired()方法该问题已反馈给作者

像很多优秀的框架在实际编码过程中发现不能满足自身要求的时候,可以使用继承重写的方式来解决实际业务中的问题

下面是业务相关的具体实现,你可以参考我的源码,

/**
 * 搜索日志缓存
 *
 * @author youyongkun
 * @date 2021/5/17 14:05
 */
@Slf4j
@Component
public class SearchLogTimedCache extends AbstractTimedCache<String, SearchLog> {

    @PostConstruct
    public void init() {
        this.schedulePrune(DEFAULT_TIMEOUT);
    }

    @PreDestroy
    public void destroy() {
        this.clean();
    }

    @Autowired
    private ISearchLogService iSearchLogService;

    @Autowired
    private Sid sid;

    @Override
    protected boolean save(Map<String, SearchLog> logsMap) {
        Set<String> keys = logsMap.keySet();
        Map<String, SearchLog> keywordMap = iSearchLogService.getByKeywordToMap(keys);
        List<SearchLog> saveList = new ArrayList();
        List<SearchLog> updateList = new ArrayList();
        keys.forEach(key -> {
            SearchLog searchLog = keywordMap == null ? null : keywordMap.get(key);
            SearchLog cacheSearchLog = logsMap.get(key);
            if (searchLog == null) {
                searchLog = new SearchLog();
                searchLog.setId(sid.nextShort());
                searchLog.setKeyWorld(key);
                searchLog.setSearchNum(cacheSearchLog.getSearchNum());
                searchLog.setIsHit(cacheSearchLog.getIsHit());
                searchLog.setUpdatedTime(LocalDateTime.now());
                searchLog.setCreatedTime(LocalDateTime.now());

                saveList.add(searchLog);
            } else {
                searchLog.setSearchNum(searchLog.getSearchNum() + cacheSearchLog.getSearchNum());
                searchLog.setIsHit(cacheSearchLog.getIsHit());
                searchLog.setUpdatedTime(LocalDateTime.now());

                updateList.add(searchLog);
            }
        });

        // 入库
        int result = 0;
        if (saveList != null && saveList.size() > 0) {
            if (iSearchLogService.saveBatch(saveList)) {
                result += 1;
            }
        } else {
            result += 1;
        }

        if (updateList != null && updateList.size() > 0) {
            if (iSearchLogService.updateBatchById(updateList)) {
                result += 1;
            }
        } else {
            result += 1;
        }
        return result == 2;
    }


    /**
     * 清除日志
     *
     * @author youyongkun
     * @date 2021/5/17 16:20
     */
    public void clean() {
        Map<String, SearchLog> logsMap = new HashMap(cacheMap.size() + 1);
        synchronized (super.cacheMap){
            Iterator<CacheObj<String, SearchLog>> iterator = super.cacheMap.values().iterator();
            while (iterator.hasNext()) {
                CacheObj<String, SearchLog> cacheObj = iterator.next();
                logsMap.put(cacheObj.getKey(), cacheObj.getValue());

                iterator.remove();
            }
        }

        if (logsMap.size() > 0) {
            if (this.save(logsMap)) {
                log.info("缓存日志入库成功,受影响{}条数,完整数据{}", logsMap.size(), logsMap);
            } else {
                log.info("缓存日志入库失败,完整数据{}", logsMap);
            }
        }
    }
}

这里的clean()方法是通过synchronized关键字来保证线程安全问题。在实际生产环境中,你无法保证在key的有效时间内你的缓存不被塞满,所以需要一个WEB API来使用清除。这就是要使用synchronized关键字的原因。

在使用 synchronized关键字来确保线程安全之前,我尝试过使用Mybatis反射工具类 org.apache.ibatis.reflection.SystemMetaObject#forObject()来直接获取 cn.hutool.cache.impl.AbstractCache#lock锁,当然这是失败的。为什么会失败呢?是因为 lock字段没有提供 getter方法,而MyBatis反射工具类只允许属性字段有 getter/setter方法才能被访问,那为什么MyBatis会有这样的限制呢?是因为当你使用Java反射直接去拿字段的Filed对象,你是无法将Filed对象转换为原本字段对应的数据类型。所以 JavaBean开发规范就是这么来的。
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
4月前
|
缓存 负载均衡 网络协议
电商API接口性能优化技术揭秘:缓存策略与负载均衡详解
电商API接口性能优化是提升系统稳定性和用户体验的关键。本文聚焦缓存策略与负载均衡两大核心,详解其在电商业务中的实践。缓存策略涵盖本地、分布式及CDN缓存,通过全量或部分缓存设计和一致性维护,减少后端压力;负载均衡则利用反向代理、DNS轮询等技术,结合动态调整与冗余部署,提高吞吐量与可用性。文中引用大型及跨境电商平台案例,展示优化效果,强调持续监控与迭代的重要性,为电商企业提供了切实可行的性能优化路径。
|
5月前
|
缓存 搜索推荐 CDN
HTTP缓存策略的区别和解决的问题
总的来说,HTTP缓存策略是一种权衡,需要根据具体的应用场景和需求来选择合适的策略。理解和掌握这些策略,可以帮助我们更好地优化网页性能,提高用户的浏览体验。
128 11
|
4月前
|
存储 缓存
.NET 6中Startup.cs文件注入本地缓存策略与服务生命周期管理实践:AddTransient, AddScoped, AddSingleton。
记住,选择正确的服务生命周期并妥善管理它们是至关重要的,因为它们直接影响你的应用程序的性能和行为。就像一个成功的建筑工地,工具箱如果整理得当,工具选择和使用得当,工地的整体效率将会大大提高。
156 0
|
7月前
|
数据采集 缓存 JavaScript
数据抓取的缓存策略:减少重复请求与资源消耗
本教程聚焦于提升爬虫效率与稳定性,通过结合缓存策略、代理IP技术(如爬虫代理)、Cookie和User-Agent设置,优化数据采集流程。以知乎为例,详细讲解如何抓取指定关键词的文章标题和内容。内容涵盖环境准备、代码实现、常见问题及解决方案,并提供延伸练习,帮助读者掌握高效爬虫技巧。适合具备Python基础的初学者,助你规避网站机制,顺利获取目标数据。
174 2
数据抓取的缓存策略:减少重复请求与资源消耗
|
12月前
|
缓存 算法 数据挖掘
深入理解缓存更新策略:从LRU到LFU
【10月更文挑战第7天】 在本文中,我们将探讨计算机系统中缓存机制的核心——缓存更新策略。缓存是提高数据检索速度的关键技术之一,无论是在硬件还是软件层面都扮演着重要角色。我们会详细介绍最常用的两种缓存算法:最近最少使用(LRU)和最少使用频率(LFU),并讨论它们的优缺点及适用场景。通过对比分析,旨在帮助读者更好地理解如何选择和实现适合自己需求的缓存策略,从而优化系统性能。
340 3
|
缓存 JavaScript 中间件
优化Express.js应用程序性能:缓存策略、请求压缩和路由匹配
在开发Express.js应用时,采用合理的缓存策略、请求压缩及优化路由匹配可大幅提升性能。本文介绍如何利用`express.static`实现缓存、`compression`中间件压缩响应数据,并通过精确匹配、模块化路由及参数化路由提高路由处理效率,从而打造高效应用。
448 102
|
11月前
|
Web App开发 缓存 UED
如何设置浏览器的缓存策略?
【10月更文挑战第23天】通过合理地设置浏览器的缓存策略,可以在提高网页性能、减少网络流量的同时,确保用户能够获取到最新的内容,从而提升用户体验和网站的性能优化效果。
887 60
|
10月前
|
缓存 API C#
C# 一分钟浅谈:GraphQL 中的缓存策略
本文介绍了在现代 Web 应用中,随着数据复杂度的增加,GraphQL 作为一种更灵活的数据查询语言的重要性,以及如何通过缓存策略优化其性能。文章详细探讨了客户端缓存、网络层缓存和服务器端缓存的实现方法,并提供了 C# 示例代码,帮助开发者理解和应用这些技术。同时,文中还讨论了缓存设计中的常见问题及解决方案,如缓存键设计、缓存失效策略等,旨在提升应用的响应速度和稳定性。
126 13
|
11月前
|
存储 缓存 安全
在 Service Worker 中配置缓存策略
Service Worker 是一种可编程的网络代理,允许开发者控制网页如何加载资源。通过在 Service Worker 中配置缓存策略,可以优化应用性能,减少加载时间,提升用户体验。此策略涉及缓存的存储、更新和检索机制。
|
11月前
|
存储 缓存 监控
利用 Redis 缓存特性避免缓存穿透的策略与方法
【10月更文挑战第23天】通过以上对利用 Redis 缓存特性避免缓存穿透的详细阐述,我们对这一策略有了更深入的理解。在实际应用中,我们需要根据具体情况灵活运用这些方法,并结合其他技术手段,共同保障系统的稳定和高效运行。同时,要不断关注 Redis 缓存特性的发展和变化,及时调整策略,以应对不断出现的新挑战。
177 10