JoinCache
很多读模型都是“一样东西加上它指向的东西”:订单与它的客户、资料扩展与它的用户。这两样你本来就都缓存了。组合视图把它们一起读出来:先从主缓存读一个值,从中推导出一个 key,再用这个 key 读关联缓存。
组合视图不存储任何东西,没有 loader,也没有 put。两个组件缓存各自保有自己的数据源、TTL 与删除。组合视图有两种:
| 继承 | 两侧 | 增加 |
|---|---|---|
JoinView<K1, V1, K2, V2> | 任意两种缓存类型(都是 ReadableCache),例如一个 Cache 配一个 Store | 无:name、get、getAll |
JoinCache<K1, V1, K2, V2> | 两个 Cache | invalidate(key) 与 invalidate(firstKey, joinKey) |
JoinCache 继承 JoinView。只有派生副本可以经视图失效;权威的一侧保留它自己的 delete。
声明
两个组件缓存都是普通缓存,可以是接口式声明,也可以是 spec bean。视图是一个继承 JoinView<K1, V1, K2, V2> 或 JoinCache<K1, V1, K2, V2> 的接口,列在 @EnableCoCache(joinCaches = …) 中。这里两侧都是 Cache,所以视图可以是 JoinCache:
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
三种方式选一种:
joinKeyExpression:SpEL 模板,只适用于String类型的关联 key。joinKeyExtractor = "beanName":任意关联 key 类型。- 都不设置:使用唯一的
JoinKeyExtractor<V1, K2>类型 bean。
@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 方法在代理上执行:
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:
val view = SimpleJoinCache("UserExtendInfoJoinCache", userExtendInfoCache, userCache) { info -> info.userId }
val typed = JoinCacheProxies.create(UserExtendInfoJoinCache::class.java, view)