跳转到内容

缓存

缓存(FunctionCache / @Cache / @CacheImpl)

缓存最佳实践和问题排查指南:这里

FunctionCache抽象接口

FunctionCache 接口是缓存资源的顶层抽象接口,所有声明了 @Cache 注解的缓存资源的接口声明都必须继承当前接口

抽象接口提供了下面几种抽象方法:

  • get:需要在子类中实现,提供缓存数据的生成逻辑:

    • 当目标缓存存在时,直接返回缓存数据
    • 当目标缓存不存在时,先执行 get 方法逻辑,将执行结果进行缓存后返回
    • 如果 get 方法返回 NULL, 则下次请求缓存时,仍然会尝试调用 get 方法,即 NULL 值不会被真的缓存
  • put:手动缓存目标数据,如果目标缓存原先已经存在数据,则会被覆盖

  • invalidate:失效目标缓存

  • clear:清空同一个缓存接口(声明了 @Cache 注解)下的所有实现类缓存的数据

package io.terminus.trantorframework.api.cache;
/**
* 声明一个资源为缓存, 行为上类似 Loading cache
* 需要实现 get 方法来声明缓存内容的获取过程
*
* @param <K> Cache Key 的类型, 需实现 CacheKey
* @param <V> 缓存的对象类型, 即需要被缓存的内容
*/
public interface FunctionCache<K, V> {
/**
* 当资源未被缓存时, 会调用此方法获取缓存
* 允许范围 null, 但是当为 null 时, 再次获取仍然会调用此方法
*
* @param key 缓存的 Key
* @return 需要被缓存的内容
*/
V get(K key);
/**
* 缓存当前资源,会根据 {@link io.terminus.trantorframework.api.annotation.Cache}.keyPath 表达式规则解析值作为缓存key
*
* @param key 缓存key解析的来源
* @param value 被缓存对象
*/
default void put(K key, V value) {
}
/**
* 根据 CacheKey 失效相应缓存, 如果对应 Key 不存在缓存内容, 则会忽略
* 该方法会在执行时由代理实现覆盖其行为
*
* @param key 需要失效缓存的 Key
*/
default void invalidate(K key) {
}
/**
* 将该缓存内的所有缓存内容全部清除
* 该方法会在执行时由代理实现覆盖其行为
*/
default void clear() {
}
}

@Cache

  • 在 Java interface 上增加该注解声明,标明这个接口是一个缓存资源的声明
  • 这个 Java interface 必须继承了FunctionCache<K, V>抽象接口
  • name:缓存名称,为了保证资源国际化能正常进行,请使用英文编写,为空时会展示缓存的 Key
  • desc:缓存描述,为了保证资源国际化能正常进行,请使用英文编写
  • expire:注解上可以声明缓存的过期时间,但这个缓存时间只针对通过 put(K key, V value) 方法手动缓存的数据生效,单位是毫秒ms,默认过期时间为60分钟
  • keyPath:如果缓存的 Key 是复杂对象,比如 BO 或者 TO,则需要声明 keyPath 规则,来返回真实用来缓存的 Key
    • 表达式语言是MVEL.
  • multiKeyPath:作用与 keyPath 一样,区别是 multiKeyPath 是数组,可以对复杂参数对象中的多个字段同时进行声明解析后将多个解析值进行拼接作为缓存key。
    • 注意:该属性从 trantor-parent 0.17.37.RELEASE 版本开始提供支持。
package io.terminus.trantorframework.api.annotation;
import io.terminus.trantorframework.api.annotation.internal.TrantorComponent;
import io.terminus.trantorframework.api.cache.FunctionCache;
import java.lang.annotation.*;
/**
* 标记一个接口成为缓存申明。
* <p>
* 缓存是用于加速读取的一种方式, 用于一些读多写少, 并且查询开销较大的场景.
* 缓存声明需要继承 {@link FunctionCache} 接口, 并声明 K/V 泛型
* <p>
* 可以使用 {@link CacheImpl} 申明缓存的实现。
*
* @author Xyf
* @author huangzijing
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TrantorComponent
public @interface Cache {
/**
* 默认 key/value Enrty 的过期时长
*/
long DEFAULT_EXPIRE_TIME = 60 * 60 * 1000;
/**
* 代表无限期存储 expire 值,不推荐使用,尽量给缓存定义一个过期时长
*/
long INFINITELY_EXPIRE_TIME = 0;
/**
* 缓存的名称, 用来资源展示使用, 为空时会展示缓存的 Key
*
* @return 缓存的名称
*/
String name() default "";
/**
* 缓存的描述, 用来描述该缓存的使用场景和注意事项等
*
* @return 缓存的详细描述
*/
String desc() default "";
/**
* 缓存的 Key 声明, 当入参是复杂对象时, 可以声明一个表达式来声明缓存的 Key
* 当为空时, 就是第一个参数的对象
* 格式是:
* 1. 如果需要获取当前参数对象的属性,直接 keyPath = {属性名} 即可
* 2. 如果需要获取多级关联对象属性,keyPath = {一级属性名}.{二级属性名},以此类推可以获取多级属性,理论上层级没有限制
* 比如有一个入参是 user, 我们要取 user 的 id, 就可以写 user.id
*
* @return 缓存的 Key 规则
*/
String keyPath() default "";
/**
* 用于承载多个 keyPath,规则与{@link #keyPath}一致,{@link #keyPath} 与 {@link #multiKeyPath()} 不允许同时使用
*
* @return 缓存的 Key 规则数组
*/
String[] multiKeyPath() default {};
/**
* 失效时间, 单位为 ms
* 默认失效时间是 60 分钟
*
* @return 失效时间
*/
long expire() default DEFAULT_EXPIRE_TIME;
/**
* 如果 {@link FunctionCache#get(Object)} 返回 NULL,是否需要缓存
*
* @return 是否缓存 NULL 对象,默认不缓存
*/
boolean cacheNullObject() default false;
}

@CacheImpl

  • 在 Java Class 上增加该注解声明,标明这个类是一个缓存资源的实现类
  • 这个 Java class 必须继承一个带有 @Cache 注解声明的缓存接口
  • name:缓存实现名称,为了保证资源国际化能正常进行,请使用英文编写,为空时会展示缓存的 Key
  • desc:缓存实现描述,为了保证资源国际化能正常进行,请使用英文编写
  • expire:注解上可以声明缓存的过期时间,但这个缓存时间只针对通过 V get(K key) 方法缓存的数据生效,单位是毫秒ms,默认过期时间为60分钟
package io.terminus.trantorframework.api.annotation;
import io.terminus.trantorframework.api.annotation.internal.TrantorComponent;
import java.lang.annotation.*;
/**
* 标记一个类成为缓存的实现,需要实现一个被 {@link Cache} 注解的接口。
*
* @author Xyf
* @author huangzijing
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TrantorComponent
public @interface CacheImpl {
long DEFAULT_EXPIRE_TIME = Cache.DEFAULT_EXPIRE_TIME;
/**
* 缓存实现的名称, 用来资源展示使用, 为空时会展示缓存实现的 Key
*
* @return 缓存的名称
*/
String name() default "";
/**
* 缓存实现的描述, 用来描述该缓存的使用场景和注意事项等
*
* @return 缓存的详细描述
*/
String desc() default "";
/**
* 失效时间, 单位为 ms
* 默认失效时间是 60 分钟
*
* @return 失效时间
*/
long expire() default DEFAULT_EXPIRE_TIME;
}

代码示例

接口

/**
* 企业微信访问凭证获取
*
* @author: terminus
*/
@Cache(name = "EnterpriseWechatAccessTokenCache")
public interface EnterpriseWechatAccessTokenCache extends FunctionCache<EnterpriseWechatConfigTO, String> {
}

实现

/**
* 过期时间2小时
*
* @author: terminus
*/
@CacheImpl(name = "EnterpriseWechatAccessTokenCacheImpl", expire = 2 * 60 * 60 * 1000)
public class EnterpriseWechatAccessTokenCacheImpl implements EnterpriseWechatAccessTokenCache {
@Override
public String get(EnterpriseWechatConfigTO key) {
return EnterpriseWechatUtils.obtainAccessToken(key.getCorpId(), key.getCorpSecret());
}
}

缓存 key 的生成规则

functionCache:{version}:{appKey}:{cache接口全限定名}

最终在redis中存储的key主要包含四部分:

  • functionCache:固定前缀,为了明确标识这个 Trantor 缓存;
  • {version}:version 会随着缓存机制的迭代有所调整,比如v1、v2、v3…,主要是用于规避可能存在的升级不兼容情况;
  • {appKey}:标记当前缓存数据是由娜一层触发生成的,比如 gaia、bbc 等,只在 Trantor 0.18 支持 app 分层设计 后引入,主要解决数据隔离目的; cache key加上appKey在分层这个设计下是合理的,cache跟function一样设计成可二开,比如gaia和bbc各有一个cacheimpl,如果没有appKey隔离,也就意味着会共用同一个key,这其实不是很符合分层后数据隔离的设计; 举个例子,比如gaia和bbc各自实现cacheimpl,gaia的cacheimpl通过DS查询数据时正常情况下会携带where bizAppKey = gaia,bbc cacheimpl 则是查询bizAppKey = bbc数据,如果cache不做appKey隔离,就会导致bbc可以拿到gaia的缓存数据,就会产生很多不符合预期的结果。
  • cache接口全限定名:每个cache接口的全限定名,用作 key 隔离

keyPath的解析

缓存方法参数不仅可以是简单类型对象,如 Long,String 等等,还可以直接传进来一个复杂对象,如 ItemBO。

如果直接传递一个复杂对象,我们可以定义keyPath规则,通过表达式解析出真实的key值,表达式采用MVEL规范。

举例:

ItemBO:

{
"id":1,
"itemName":"奶油草莓",
"itemNumber":"GN-00001",
"itemType":"food",
"brand":{
"brandName":"下沙",
"id":124
}
}
  1. keyPath 为缓存声明 @Cache 上配置的 key 解析规则,cache-key 为解析结果。

    • keyPath = “id”:cache-key = “id:1”
    • keyPath = “itemNumber”:cache-key = ”itemNumber:GN-00001“
    • keyPath = “brand.id”:cache-key = “brand.id:124”
    • 未配置keyPath:缓存key为整个ItemBO对象的 JSON 串

    注意:在 trantor-parent 0.17.37.RELEASE 之前的版本由于在未配置keyPath时将整个复杂对象的 toString() 结果作为缓存key,这样会出现如果调用了 Object.toString() 返回对象 HashCode 每次都是不同的,因为每次反序列化出的对象都不是同一个,导致每次都只走 get 方法逻辑,而不走缓存。

  2. multiKeyPath 可以理解为多个 keyPath 的数组模式,所以它的的用法和解析原理跟 keyPath 一致,只是会对多个 keyPath 解析结果的数组进行 Join 拼接,连接符为英文字符的冒号 :

    • multiKeyPath = {“id”, “itemNumber”}:cache-key = “id:1:itemNumber:GN-00001”

缓存存储结构

Trantor缓存机制采用了 Redis 作为集中式缓存存储介质,并且每个缓存声明对应于 Redis 中的一个 Hash 数据结构,之所以采用 Hash 结构,有以下几个原因:

  1. 由于一个缓存声明 Cache 可以存在多个实现 CacheImpl,并且同一个实现类解析得到的 cache-key 也会存在差异,很好的契合了 Hash 的结构特点
  2. 为了支持 invalidateclear 方法的处理效率,Hash 结构不仅对于单一 value 的删除较为方便,而且对于所有 value (等价于整个 Hash)的删除操作,相比较线性结构在实现和性能表现上都更加优秀。

image-20220729103503408

SimpleKeyItemBrandCache 缓存为例:

  • 先假设接口全限定名为 io.terminus.trantor.example.cache.SimpleKeyItemBrandCache
  • 为了跟其他缓存数据做区分,我们固定一个 key-prefixfunctionCache:v3: ,并且 1.0 引入了 app 分层设计,所以也需要实现appKey数据隔离。假设触发cache的请求携带的appKey是gaia,最终整个 Hash 结构的 key 为 functionCache:v3:gaia:io.terminus.trantor.example.cache.SimpleKeyItemBrandCache
  • 下图中 **1001、1002、…**是具体传递的参数 id ,或者是经由 keyPathMVEL 解析出来的值

image-20220729103644166

缓存调用时序图

缓存调用时序图