Skip to content

JoinCache ​

很多读模型都是“一样东西加上它指向的东西”:订单与它的客户、资料扩展与它的用户。这两样你本来就都缓存了。组合视图把它们一起读出来:先从主缓存读一个值,从中推导出一个 key,再用这个 key 读关联缓存。

组合视图不存储任何东西,没有 loader,也没有 put。两个组件缓存各自保有自己的数据源、TTL 与删除。组合视图有两种:

继承两侧增加
JoinView<K1, V1, K2, V2>任意两种缓存类型(都是 ReadableCache),例如一个 Cache 配一个 Store无:name、get、getAll
JoinCache<K1, V1, K2, V2>两个 Cacheinvalidate(key) 与 invalidate(firstKey, joinKey)

JoinCache 继承 JoinView。只有派生副本可以经视图失效;权威的一侧保留它自己的 delete。

声明 ​

两个组件缓存都是普通缓存,可以是接口式声明,也可以是 spec bean。视图是一个继承 JoinView<K1, V1, K2, V2> 或 JoinCache<K1, V1, K2, V2> 的接口,列在 @EnableCoCache(joinCaches = …) 中。这里两侧都是 Cache,所以视图可以是 JoinCache:

kotlin
data class UserExtendInfo(val id: String, val userId: String)

@CoCache
interface UserExtendInfoCache : Cache<String, UserExtendInfo>

@CoCache
interface UserCache : Cache<String, User>

@JoinCacheable(
    firstCacheName = "UserExtendInfoCache",
    joinCacheName = "UserCache",
    joinKeyExpression = "#{#root.userId}",   // 以主值为根对象求值
)
interface UserExtendInfoJoinCache : JoinCache<String, UserExtendInfo, String, User>

@EnableCoCache(
    caches = [UserExtendInfoCache::class, UserCache::class],
    joinCaches = [UserExtendInfoJoinCache::class],
)
@SpringBootApplication
class App

像注入其它 bean 一样注入 UserExtendInfoJoinCache。它的 bean 名就是视图名。一侧不是 Cache,或者不希望视图的调用方做失效时,改为继承 JoinView。

@JoinCacheable 属性含义
name视图名,也是 bean 名。默认取接口简单类名。
firstCacheName必填。 主缓存的名字(接口式缓存的缓存名,或 spec 缓存的缓存名 / bean 名)。
joinCacheName必填。 关联缓存,规则相同。
joinKeyExpression以主值为根对象的 SpEL 模板,如 #{#root.userId} 或 #{userId}。要求 K2 = String。
joinKeyExtractor一个 JoinKeyExtractor<V1, K2> 的 bean 名。

关联 key ​

三种方式选一种:

  1. joinKeyExpression:SpEL 模板,只适用于 String 类型的关联 key。
  2. joinKeyExtractor = "beanName":任意关联 key 类型。
  3. 都不设置:使用唯一的 JoinKeyExtractor<V1, K2> 类型 bean。
kotlin
@Bean
fun userIdOf() = JoinKeyExtractor<UserExtendInfo, String> { it.userId }

关联 key 为 null 表示“没有可关联的”:结果中 joinKey = null、secondValue = null,并且不读取关联缓存。表达式求值为 null 时同样如此。

每个调用做什么 ​

调用行为
get(key)用主缓存的 get(key) 读取(Cache 未命中时加载)。不存在或是负缓存:返回 null。否则提取关联 key 并读关联缓存;关联值不存在时 secondValue = null。
getAll(keys)对主缓存一次 getAll,再对去重后的非空关联 key 做一次关联缓存的 getAll(没有关联 key 时跳过)。结果只包含主值存在的 key。
invalidate(key)(仅 JoinCache)只失效主缓存。 关联值被其它条目共享,由它自己的写入方负责。
invalidate(firstKey, joinKey)(仅 JoinCache)两者都失效。一方抛出时另一方照常执行,异常合并后抛出。

因为 getAll 就是两次批量读取,列出指向 20 个用户的 100 条扩展信息,每个缓存最多只需一次批量 Redis 读取和一次 loadAll,与有多少条目共享同一个用户无关。

保证与限制 ​

  • 每次读取都经过组件缓存,它们的保证原样适用:受栅栏保护的加载、失效、localMaxAge。
  • 组件缓存抛出的异常原样传出。没有答案的读取(例如 Redis 故障时读 Store)抛出它的 CacheReadException(见读取:存在、不存在与未知)。
  • 两个缓存之间没有快照。 两次读取相互独立,组合出的一对值可能来自不同时刻。如果主值在两次读取之间换了关联 key,在主缓存被失效之前你会看到旧的配对。
  • User 变化时,在 UserCache 上失效它。所有读取它的视图在下一次调用时都会看到变化。

添加方法 ​

default 方法在代理上执行:

kotlin
interface UserExtendInfoJoinCache : JoinCache<String, UserExtendInfo, String, User> {
    fun userOf(id: String): User? = get(id)?.secondValue
}

对应不上所继承类型(JoinView 或 JoinCache)任何方法的抽象方法会让启动失败。

启动检查 ​

以下情形启动失败,错误信息指出视图与属性:

  • firstCacheName 或 joinCacheName 为空,或对应不上任何缓存,
  • 指定的缓存对 JoinCache 不是 Cache<K1, V1>(或 Cache<K2, V2>),对 JoinView 不是 ReadableCache<K1, V1>(或 ReadableCache<K2, V2>),
  • 接口的类型参数不是具体类型,或缺少 @JoinCacheable,
  • joinKeyExpression 中没有 #{…}(会成为常量,所有值都关联到同一个 key),或 joinKeyExtractor 指定的 bean 不是 JoinKeyExtractor<V1, K2>,
  • 同时设置了 joinKeyExpression 与 joinKeyExtractor、对非 String 的关联 key 使用 joinKeyExpression,或按类型查找提取器时找到零个或多个 bean。

不用 Spring ​

cocache-core 中的 SimpleJoinView(任意两个 ReadableCache)与 SimpleJoinCache(两个 Cache)是实现,JoinCacheProxies 把其中一个包装成你的接口。继承 JoinCache 的接口需要 SimpleJoinCache:

kotlin
val view = SimpleJoinCache("UserExtendInfoJoinCache", userExtendInfoCache, userCache) { info -> info.userId }
val typed = JoinCacheProxies.create(UserExtendInfoJoinCache::class.java, view)

基于 Apache License 2.0 发布。