likes
comments
collection
share

低侵入内存缓存架构实现

作者站长头像
站长
· 阅读数 3

前言

前面聊了一些内存缓存相关的知识点:

  • 了解缓存技术

  • 常用的内存缓存框架

  • 针对caffeine框架实现原理进行源码分析

  • 缓存在使用过程中常见的问题及如何应对

那么对于如何使用缓存以及缓存的实现原理在这里先告一段落了

但是大家有没有想过一个问题,如果说已经上线了的系统,之前并没有使用缓存技术

而现在因为系统性能问题需要利用缓存技术对现有的功能进行优化

这时候应该如何设计才能保证在不影响现有功能的前提下最大程度的降低开发成本

我们先来回顾一下使用缓存的常用操作

  • 查询数据时,先读取缓存,如果缓存没有数据则查询数据库并将结果写入缓存.如果缓存有数据直接返回缓存中的数据
  • 新增数据时,将数据写入缓存
  • 删除数据时,删除对应的缓存数据

如果说在现有的系统中对每个接口新加操作缓存的逻辑

无疑是加大了开发人员的工作量和降低了程序的可读性,也违背了设计原则中的单一职责

这时候可以考虑通过Spring AOP和注解实现低侵入的缓存框架

在spring中已经有框架实现了这个功能(spring cache)

spring cache框架提供了一系列的注解,介绍一下常用的注解

  • @Cacheable 主要针对方法配置,能够根据方法的请求参数对其进行缓存
  • @CacheEvict 清除缓存
  • @CachePut 保证方法被调用,又希望结果被缓存.与@Cacheable区别在于是否每次都调用方法,常用于更新

对于这几个注解的简单使用,下面提供一个示例进行参考

/**
* 查询时先查询缓存,缓存中找不到再查询数据库
* 将数据库查询结果进行缓存(缓存的key=该注解上的value+::key)
*/
@Cacheable(value = "LoginAccount::account", key = "#account")
@Override
public LoginAccount getByAccount(String account) {
    return super.lambdaQuery()
        .eq(LoginAccount::getAccount, account)
        .one();
}

/**
* 新增数据成功后,将该数据进行缓存
*/
@CachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount add(LoginAccount loginAccount) {
    super.save(loginAccount);
    return loginAccount;
}

/**
* 修改数据成功后,将该数据进行缓存
*/
@CachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount update(LoginAccount loginAccount) {
    super.updateById(loginAccount);
    return loginAccount;
}

/**
* 删除数据库成功后将缓存中该账号的数据一起删除
*/
@CacheEvict(value = "LoginAccount::account", key = "#account", condition = "#result == true")
public boolean deleteByAccount(String account) {
    return super.lambdaUpdate()
        .eq(LoginAccount::getAccount, account)
        .remove();
}

自定义缓存框架

对于这几个注解的使用,相信大家在看完了这个简单的实例或者查阅相关的资料都能很快的上手

但是本次主要介绍的是如何通过SpringAop结合自定义注解去封装一个类似spring cache这样的缓存框架

自定义注解

以@Cacheable、@CachePut、@CacheEvict作为参考,我们先要自定义几个这样的注解

@HTCCacheable

/**
 * <p>
 * 自定义缓存读取注解
 * </p>
 *
 * @author 昊天锤
 * @date 2020/10/24 0024 19:31
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HTCCacheable {

    /**
     * 缓存名称
     */
    String cacheName() default "";

    /**
     * 缓存key名称
     */
    String cacheKey();

    /**
     * 缓存有效时间(单位:秒) 默认1个小时
     */
    int expire() default 3600;

    /**
     * 缓存主动刷新时间(单位:秒),默认-1表示不主动刷新
     */
    int refresh() default -1;
}

@HTCCacheEvict

/**
 * <p>
 * 自定义缓存清除注解
 * </p>
 *
 * @author 昊天锤
 * @date 2020/10/24 0024 19:32
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HTCCacheEvict {

    /**
     * 缓存名称
     */
    String cacheName() default "";

    /**
     * 缓存key名称
     */
    String cacheKey();

    /**
     * 是否需要清除cacheName的全部数据,默认不需要
     */
    boolean allEntries() default false;
}

@HTCCachePut

/**
 * <p>
 * 自定义缓存清除注解
 * </p>
 *
 * @author 昊天锤
 * @date 2020/10/24 0024 19:32
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented 
public @interface HTCCacheEvict {

    /**
     * 缓存名称
     */
    String cacheName() default "";

    /**
     * 缓存key名称
     */
    String cacheKey();

    /**
     * 是否需要清除cacheName的全部数据,默认不需要
     */
    boolean allEntries() default false;
}

配置缓存具体实现

自定义好缓存注解以后,我们需要选择一个内存缓存框架作为具体的缓存实现

之前介绍过的内存缓存有以下几个

  • JDK的Map
  • ehcache
  • guava
  • caffeine

前面也说到了caffeine是目前性能最好的内存缓存框架,所以本篇文章将会使用caffeine作为具体的缓存实现

选择了caffeine就需要配置caffeine的实现,前面的文章介绍了caffeine的加载方式

  • 手动加载
  • 同步加载
  • 异步加载

这里将使用手动加载的方式来创建一个caffeine的实现,这里便通过spring配置类的方式来配置caffeine缓存

/**
 * <p>
 * 自定义缓存配置,spring配置caffeine
 * </p>
 *
 * @author 昊天锤
 * @date 2020/10/24 0024 19:49
 */
@Slf4j
@Configuration
public class HTCCacheConfig {

    /**
     * 配置使用手动加载的方式来创建一个caffeine的实现
     */
    @Bean("caffeineCache")
    public Cache cache() {
        return Caffeine.newBuilder()
                // 缓存最大记录数,超过就会被驱逐
                .maximumSize(500)
                // 基于时间失效,写入之后开始计时失效
                .expireAfterWrite(1, TimeUnit.HOURS)
                // 缓存记录统计
                .recordStats()
                // 缓存移除监听器
                .removalListener((key, value, cause) -> log.debug("缓存失效了 removed:{} cause:{}", key, cause))
                // 使用手动加载方式
                .build();
    }
}

自定义缓存AOP切面实现

这时候我们要准备一个生成缓存key的工具类并且支持spring EL表达式

还要AOP切面,对使用了我们自定义缓存注解的方法进行功能加强

KeyGenerator

/**
 * <p>
 * key的生成策略:缓存名+缓存key(支持spring EL表达式)
 * </p>
 *
 * @author 昊天锤
 * @date 2020/10/24 0024 20:34
 */
@Component
public class KeyGenerator {


    /**
     * 用于springEL表达式的解析
     */
    private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();

    /**
     * 用于获取方法参数定义的名字
     */
    private DefaultParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();


    public String generatorKey(ProceedingJoinPoint point, String cacheName, String cacheKey) throws NoSuchMethodException {
        if (StringUtils.isEmpty(cacheKey)) {
            throw new NullPointerException("cacheKey不能为空");
        }

        // 缓存名为空时,默认赋值为该方法的包名点类名点方法名
        Signature signature = point.getSignature();
        if (StringUtils.isEmpty(cacheName)) {
            cacheName = signature.getDeclaringTypeName() + "." + signature.getName();
        }

        // 获取目标方法
        Method method = point.getTarget()
                .getClass()
                .getMethod(signature.getName(), ((MethodSignature) signature).getParameterTypes());

        // 设置参数
        String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(method);
        Object[] args = point.getArgs();
        EvaluationContext evaluationContext = new StandardEvaluationContext();
        for (int i = 0; i < parameterNames.length; i++) {
            evaluationContext.setVariable(parameterNames[i], args[i]);
        }

        // 解析cacheKey
        String parseCacheKey = spelExpressionParser.parseExpression(cacheKey).getValue(evaluationContext, String.class);
        return cacheName + "::" + parseCacheKey;
    }
}

缓存注解搭配的AOP切面

/**
 * <p>
 * 自定义缓存AOP切面实现
 * </p>
 *
 * @author 昊天锤
 * @date 2020/10/24 0024 20:06
 */
@Slf4j
@Aspect
@Component
public class HTCCacheAspect {

    @Resource
    private Cache caffeineCache;

    @Resource
    private KeyGenerator keyGenerator;

    /**
     * 定义环绕通知,读取缓存数据
     * 1.解析缓存注解的参数
     * 2.查询缓存,有数据直接返回
     * 3.查询不到缓存则执行目标方法,并将返回结果存入缓存
     */
    @Around(value = "@annotation(htcCacheable)")
    public Object cached(ProceedingJoinPoint point, HTCCacheable htcCacheable) throws Throwable {
        String key = keyGenerator.generatorKey(point, htcCacheable.cacheName(), htcCacheable.cacheKey());
        Object cacheVo = caffeineCache.getIfPresent(key);
        if (cacheVo != null) {
            return SerializationUtils.deserialize((byte[]) cacheVo);
        } else {
            try {
                Object vo = point.proceed();
                caffeineCache.put(key, SerializationUtils.serialize(vo));
                return vo;
            } catch (Throwable throwable) {
                log.error("缓存读取操作异常:{}", throwable);
                throw new RuntimeException("缓存读取操作异常", throwable);
            }
        }
    }


    /**
     * 定义环绕通知,将返回值设置到缓存
     * 1.执行目标方法
     * 2.将结果集设置到缓存中
     */
    @Around(value = "@annotation(htcCachePut)")
    public Object cached(ProceedingJoinPoint point, HTCCachePut htcCachePut) {
        try {
            Object vo = point.proceed();
            String key = keyGenerator.generatorKey(point, htcCachePut.cacheName(), htcCachePut.cacheKey());
            caffeineCache.put(key, SerializationUtils.serialize(vo));
            return vo;
        } catch (Throwable throwable) {
            log.error("缓存写入操作异常:{}", throwable);
            throw new RuntimeException("缓存写入操作异常", throwable);
        }
    }


    /**
     * 定义环绕通知,删除对应的缓存数据
     * 1.执行目标方法
     * 2.删除对应的缓存数据
     */
    @Around(value = "@annotation(hTCCacheEvict)")
    public Object cached(ProceedingJoinPoint point, HTCCacheEvict hTCCacheEvict) {
        try {
            Object vo = point.proceed();
            String key = keyGenerator.generatorKey(point, hTCCacheEvict.cacheName(), hTCCacheEvict.cacheKey());
            caffeineCache.invalidate(key);
            return vo;
        } catch (Throwable throwable) {
            log.error("缓存删除操作异常:{}", throwable);
            throw new RuntimeException("缓存删除操作异常", throwable);
        }
    }
}

使用自定义缓存缓存

当把一切就准备就绪以后,就可以将使用spring cache的缓存注解换成我们自定义的缓存框架看下效果.示例如下

/**
* 查询时先查询缓存,缓存中找不到再查询数据库
* 将数据库查询结果进行缓存(缓存的key=该注解上的value+::key)
*/
@HTCCacheable(value = "LoginAccount::account", key = "#account")
@Override
public LoginAccount getByAccount(String account) {
    return super.lambdaQuery()
        .eq(LoginAccount::getAccount, account)
        .one();
}

/**
* 新增数据成功后,将该数据进行缓存
*/
@HTCCachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount add(LoginAccount loginAccount) {
    super.save(loginAccount);
    return loginAccount;
}

/**
* 修改数据成功后,将该数据进行缓存
*/
@HTCCachePut(value = "LoginAccount::account", key = "#loginAccount.account")
public LoginAccount update(LoginAccount loginAccount) {
    super.updateById(loginAccount);
    return loginAccount;
}

/**
* 删除数据库成功后将缓存中该账号的数据一起删除
*/
@HTCCacheEvict(value = "LoginAccount::account", key = "#account", condition = "#result == true")
public boolean deleteByAccount(String account) {
    return super.lambdaUpdate()
        .eq(LoginAccount::getAccount, account)
        .remove();
}

结语

自定义缓存框架的目的并不是为了颠覆或者说替代spring cache框架

而是为了让自己更加了解spring cache框架是如何实现的,使用的过程中遇到了问题应该从哪些方面去思考

至此内存缓存技术的介绍就到此结束了,谢谢大家观看