search.png
关于我
menu.png
java 实现的数据查询缓存通用模型——缓存模型核心AOP实现(3)

收录于墨的2020~2021开发经验总结

接续上文(java 实现的数据查询缓存通用模型——缓存清除cacheEvict(2)

4、缓存模型核心AOP实现

4.1 概述

在上两篇文章中,我们主要分析了 @DawnCacheable 实现的缓存,和@DawnCacheEvict实现的缓存清除,之前是从模型上分析的,还没有介绍如何实现这个模型,这篇文章就是分析这两个注解背后的实现。

Spring拥有两个核心,一个是IOC,控制反转和依赖注入,还有一个就是AOP,面向切面编程。在我这几年的编程生涯中,确实也意味到了,AOP是大部分框架的实现核心。AOP配合注解可以实现很优雅的架构设计。

对于DAWN的缓存模型,我们设计了三个注解,因此切点Point Cut也需要三个:

    @Pointcut("@annotation(cn.hengyumo.dawn.core.cache.core.DawnCacheable)")
    public void dawnCacheablePointCut() {}

    @Pointcut("@annotation(cn.hengyumo.dawn.core.cache.core.DawnCacheEvict)")
    public void dawnCacheEvictPointCut() {}

    @Pointcut("@annotation(cn.hengyumo.dawn.core.cache.core.DawnCacheEvicts)")
    public void dawnCacheEvictsPointCut() {}

4.2 DawnCacheable切面

切面我们选用了@Around,因为它可以操作目标方法获取其返回值,并且在执行方法和执行方法之后都可以加入自己想做的事情:

    @Around(value = "dawnCacheablePointCut()")
    public Object aroundDawnCacheablePointCut(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        DawnCacheable dawnCacheable = AnnotationUtils.getAnnotation(method, DawnCacheable.class);
        // 因为AOP拦截的就是@DawnCacheable 所以肯定能取到这个注解,以下这行是避开IDEA的判空警告
        Assert.notNull(dawnCacheable, "never null");
        log.debug("注解式拦截" + dawnCacheable.toString());
        // 生成Key
        String key = getDawnCacheKeyCreator(dawnCacheable.cacheKeyCreator())
                .createCacheKey(getRequest(), proceedingJoinPoint.getTarget(),
                        signature.getMethod(), getParams(proceedingJoinPoint));
        Object result;
        // 如果Key 不为空,则走入缓存流程
        if (StringUtil.isNotEmpty(key)) {
            // 如果需要同步,则加锁
            if (dawnCacheable.sync()) {
                synchronized (key.intern()) {
                    // 获取缓存或者执行方法并写入缓存
                    result = getFromCacheOrSearch(dawnCacheable, key, proceedingJoinPoint);
                }
            } else {
                result = getFromCacheOrSearch(dawnCacheable, key, proceedingJoinPoint);
            }
            return result;
        }
        return proceedingJoinPoint.proceed();
    }

@Around 获取的是 ProceedingJoinPoint类型的切点,可以通过MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();获取到切点的签名。通过proceedingJoinPoint.proceed()执行方法。

synchronized (key.intern())加锁的目的是为了使得同一时间只有一个请求可以执行真查询,而其他请求需要等这个请求执行完之后去读缓存,这个设计的解释在java 实现的数据查询缓存通用模型(1)——概述和注解设计做了描述,需要的可以去看看。

getFromCacheOrSearch 首先尝试从缓存读取数据,如果没有读到则会从数据库进行查询。此处设计可能会有缓存穿透的风险。在实际方法中,如果返回的是空,那么尽量别返回null,而返回返回空列表、空字符串。

    /**
     * 从缓存中取数据或者执行查询写入缓存
     *
     * @param dawnCacheable       注解配置
     * @param key                 缓存的键
     * @param proceedingJoinPoint 切点
     * @return 结果
     * @throws Throwable 切点proceed原样抛出
     */
    private Object getFromCacheOrSearch(DawnCacheable dawnCacheable, String key,
                                        ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result = getFromCache(key, dawnCacheable.cacheType());
        if (result == null) {
            result = proceedingJoinPoint.proceed();
            long timeToLiveSeconds = dawnCacheable.cacheTimeUnit().toSeconds(dawnCacheable.expire());
            setCache(key, result, timeToLiveSeconds, dawnCacheable.cacheType());
        } else {
            log.info("缓存命中:" + key);
        }
        return result;
    }

getFromCache 从缓存工厂中获取缓存组件,然后根据Key获取缓存:

    private Object getFromCache(String key, DawnCacheType dawnCacheType) {
        return dawnCacheFactory.getCache(dawnCacheType).getFromCache(key);
    }

为了对付缓存穿透的可能,后续可以考虑的优化是,将 Object getFromCache(String key);这个缓存获取方法增加一个状态标识:


    /**
     * 从缓存中取值
     *
     * @param key 键
     * @return Pair<Boolean, Optional<?>>, Boolean是标识位,标识是否缓存中存在这个Key
     * 该设计是为了避免空值时出现缓存穿透的可能
     * Optional 包装是因为Pair内的值都不能为null
     */
    Pair<Boolean, Optional<?>> getFromCache(String key);

getFromCacheOrSearch方法利用这个bool标识,如果是true,那么缓存中是存在这个Key的。通过这样避免了缓存穿透的可能。

    private Object getFromCacheOrSearch(DawnCacheable dawnCacheable, String key,
                                        ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Pair<Boolean, Optional<?>> result = getFromCache(key, dawnCacheable.cacheType());
        Boolean cacheContainsKey = result.getFirst();
        Assert.notNull(cacheContainsKey, "never null");
        Object obj = result.getSecond().orElse(null);
        if (!cacheContainsKey) {
            obj = proceedingJoinPoint.proceed();
            long timeToLiveSeconds = dawnCacheable.cacheTimeUnit().toSeconds(dawnCacheable.expire());
            setCache(key, obj, timeToLiveSeconds, dawnCacheable.cacheType());
        } else {
            log.info("缓存命中:" + key);
        }
        return obj;
    }

4.3 DawnCacheEvict切面

DawnCacheEvict切面拦截拥有@DawnCacheEvict注解的方法,其主要功能是在方法执行之后根据缓存清除规则配置的清除缓存。

    @Around(value = "dawnCacheEvictPointCut()")
    public Object aroundDawnCacheEvictPointCut(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        DawnCacheEvict dawnCacheEvict = AnnotationUtils.getAnnotation(method, DawnCacheEvict.class);
        Assert.notNull(dawnCacheEvict, "never null");
        log.debug("注解式拦截" + dawnCacheEvict.toString());
        Object result = proceedingJoinPoint.proceed();
        handleDawnCacheEvict(proceedingJoinPoint, dawnCacheEvict);
        return result;
    }

handleDawnCacheEvict 方法根据配置执行缓存清除:

    private void handleDawnCacheEvict(ProceedingJoinPoint proceedingJoinPoint,
                                      DawnCacheEvict dawnCacheEvictParam) {
        Class<?> target = proceedingJoinPoint.getTarget().getClass();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        String name = dawnCacheEvictParam.name();
        DawnCacheEvict[] dawnCacheEvicts = new DawnCacheEvict[]{dawnCacheEvictParam};
        if (StringUtil.isNotEmpty(name)) {
            // 根据名称获取对应的缓存清除规则配置,这里这样设计是为了支持别名机制
            dawnCacheEvicts = getDawnCacheEvictsForTarget(target, name);
        } else if (dawnCacheEvictIsNotConfig(dawnCacheEvictParam)) {
            Method method = signature.getMethod();
            log.warn("存在无名无配置的缓存清除注解:{}.{}", target.getName(), method.getName());
            return;
        }
        // 遍历
        for (DawnCacheEvict dawnCacheEvict : dawnCacheEvicts) {
            // 生成要清除的缓存的前缀
            String key = getDawnCacheKeyCreator(dawnCacheEvict.cacheKeyCreator())
                    .createCacheEvictKey(
                            dawnCacheEvict.method(), getRequest(), proceedingJoinPoint.getTarget(),
                            signature.getMethod(), getParams(proceedingJoinPoint));
            if (StringUtil.isNotEmpty(key)) {
                // 执行清除
                int delCount = deleteCache(key, dawnCacheEvict.cacheType());
                log.info("清除缓存,前缀-" + key + " 数量:" + delCount);
            }
        }
    }

getDawnCacheEvictsForTarget根据名称获取对应的缓存清除规则配置,这样设计是为了支持别名机制,具体的设计描述可以参看本系列的第二篇java 实现的数据查询缓存通用模型——缓存清除cacheEvict(2)的3.4别名机制。

dawnCacheEvictIsNotConfig用于判断注解是不是什么属性都没有配置,例如这样的:@DawnCacheEvict,未配置任何属性的清除规则是不合理的。故而会报出警告,并且不处理。

deleteCache 方法从工厂中获取缓存组件并执行删除缓存的操作。

    private int deleteCache(String keyPrefix, DawnCacheType dawnCacheType) {
        return dawnCacheFactory.getCache(dawnCacheType).deleteCache(keyPrefix);
    }

4.4 DawnCacheEvicts切面

@DawnCacheEvicts可以看作是一个@DawnCacheEvict的列表,所以切面的流程就是遍历它,然后挨个重复4.3的DawnCacheEvict切面的流程,代码如下:

    @Around(value = "dawnCacheEvictsPointCut()")
    public Object aroundDawnCacheEvictsPointCut(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Class<?> target = proceedingJoinPoint.getTarget().getClass();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        DawnCacheEvicts dawnCacheEvicts = AnnotationUtils.getAnnotation(method, DawnCacheEvicts.class);
        Assert.notNull(dawnCacheEvicts, "never null");
        log.debug("注解式拦截" + dawnCacheEvicts.toString());
        Object result = proceedingJoinPoint.proceed();
        DawnCacheEvict[] dawnCacheEvictArr = dawnCacheEvicts.dawnCacheEvicts();
        String dawnCacheEvictsName = dawnCacheEvicts.name();
        // 此处代码处理那些只有名称的“别名”型注解,如@DawnCacheEvicts("aaa")
        if (dawnCacheEvictArr.length == 0) {
            if (StringUtil.isEmpty(dawnCacheEvictsName)) {
                log.warn("存在无名无配置的缓存清除注解:{}.{}", target.getName(), method.getName());
                return result;
            }
            dawnCacheEvictArr = getDawnCacheEvictsForTarget(target, dawnCacheEvictsName);
        }
        if (dawnCacheEvictArr.length > 0) {
            // 遍历,挨个处理
            for (DawnCacheEvict dawnCacheEvict : dawnCacheEvictArr) {
                handleDawnCacheEvict(proceedingJoinPoint, dawnCacheEvict);
            }
        }
        return result;
    }

通过这三个切面,实现了对缓存注解配置的处理,这样的处理过程使用者是无感知的,是不可见的。这也是AOP设计的优雅之处。

END(3)

下一篇:java 实现的数据查询缓存通用模型——缓存组件设计(4)

我还是忍不住会想你。

——平

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2021年06月13日 14:23:00

评论区#

还没有评论哦,期待您的评论!

关闭特效