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

简介: 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开发规范就是这么来的。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
存储 缓存 算法
缓存淘汰策略
缓存淘汰策略
31 0
|
1月前
|
存储 缓存 NoSQL
后端开发中的缓存策略:提升应用性能的关键
后端开发中的缓存策略:提升应用性能的关键
24 0
|
1月前
|
缓存 监控 NoSQL
解析Redis缓存雪崩及应对策略
解析Redis缓存雪崩及应对策略
|
2月前
|
存储 缓存 UED
缓存策略与Apollo:优化网络请求性能
缓存策略与Apollo:优化网络请求性能
|
3月前
|
缓存 NoSQL 关系型数据库
mysql缓存策略
mysql缓存策略
28 0
|
3月前
|
缓存 NoSQL 关系型数据库
|
20天前
|
缓存 关系型数据库 MySQL
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
|
1天前
|
存储 缓存 安全
基于iOS平台的高效图片缓存策略实现
【4月更文挑战第22天】 在移动应用开发中,图片资源的加载与缓存是影响用户体验的重要因素之一。尤其对于iOS平台,由于设备存储空间的限制以及用户对流畅性的高要求,设计一种合理的图片缓存策略显得尤为关键。本文将探讨在iOS环境下,如何通过使用先进的图片缓存技术,包括内存缓存、磁盘缓存以及网络请求的优化,来提高应用的性能和响应速度。我们将重点分析多级缓存机制的设计与实现,并对可能出现的问题及其解决方案进行讨论。
|
1天前
|
存储 缓存 算法
实现iOS平台的高效图片缓存策略
【4月更文挑战第22天】在移动应用开发中,图片资源的处理是影响用户体验的重要因素之一。特别是对于图像资源密集型的iOS应用,如何有效地缓存图片以减少内存占用和提升加载速度,是开发者们面临的关键挑战。本文将探讨一种针对iOS平台的图片缓存策略,该策略通过结合内存缓存与磁盘缓存的机制,并采用先进的图片解码和异步加载技术,旨在实现快速加载的同时,保持应用的内存效率。
|
7天前
|
缓存 NoSQL Java
使用Redis进行Java缓存策略设计
【4月更文挑战第16天】在高并发Java应用中,Redis作为缓存中间件提升性能。本文探讨如何使用Redis设计缓存策略。Redis是开源内存数据结构存储系统,支持多种数据结构。Java中常用Redis客户端有Jedis和Lettuce。缓存设计遵循一致性、失效、雪崩、穿透和预热原则。常见缓存模式包括Cache-Aside、Read-Through、Write-Through和Write-Behind。示例展示了使用Jedis实现Cache-Aside模式。优化策略包括分布式锁、缓存预热、随机过期时间、限流和降级,以应对缓存挑战。

热门文章

最新文章