计算缓存
所谓计算缓存,指把当前对象id映射为用户定义的复杂计算属性的计算值。
计算属性回顾
在复杂计算属性一文中,我们详细地讲解了复杂计算属性。
在本文中,我们将为复杂计算属性中定义的计算属性BookStore.avgPrice
添加缓存支持。
为简化文档,本文只讨论BookStore.avgPrice
,不讨论另外一个关联型计算属性BookStore.newestBooks
,读者可阅读和运行如下官方例子
- jimmer-examples/java/jimmer-sql
- jimmer-examples/java/jimmer-sql-graphql
- jimmer-examples/kotlin/jimmer-sql-kt
- jimmer-examples/kotlin/jimmer-sql-graphql-kt
启用计算缓存
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
return new CacheFactory() {
@Override
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
...省略代码...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
...省略代码...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
...省略代码...
}
@Override
public Cache<?, ?> createResolverCache(ImmutableProp prop) {
return createPropCache(
prop,
Duration.ofHours(1),
Duration.ofHours(24)
);
}
private <K, V> Cache<K, V> createPropCache(
ImmutableProp prop,
Duration caffeineDuration,
Duration redisDuration
) {
return new ChainCacheBuilder<>()
.add(
CaffeineValueBinder
.forProp(prop)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
};
}
@Bean
fun cacheFactory(
connectionFactory: RedisConnectionFactory,
objectMapper: ObjectMapper
): KCacheFactory {
return object: KCacheFactory {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
...省略代码...
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
...省略代码...
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
...省略代码...
override fun createResolverCache(prop: ImmutableProp): Cache<*, *> =
return createPropCache(
prop,
Duration.ofHours(1),
Duration.ofHours(24)
)
private fun <K, V> createPropCache(prop: ImmutableProp, duration: Duration): Cache<K, V> =
ChainCacheBuilder<Any, Any>()
.add(
CaffeineValueBinder
.forProp(prop)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build()
}
}
使用
- Java
- Kotlin
BookStoreTable table = Tables.BOOK_STORE_TABLE;
List<BookStore> stores = sqlClient
.createQuery(table)
.select(
table.fetch(
Fetchers.BOOK_STORE_FETCHER
.allScalarFields()
.avgPrice()
)
)
.execute();
System.out.println(stores);
val stores = sqlClient
.createQuery(BookStore::class) {
select(
table.fetchBy {
allScalarFields()
avgPrice()
}
)
}
.execute()
println(stores)
-
第一步:查询聚合根
首先查询聚合根对象,执行如下SQL
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.WEBSITE
from BOOK_STORE tb_1_这里实现了代码中的查询,得到了一些BookStore对象。这种被用户直接查询而得的对象叫做聚合根对象
警告Jimmer不会对用户查询返回的聚合对象进 行缓存,因为这种查询结果的一致性无法保证。 即便需要以牺牲一致性为代价对其缓存,也是用户的业务问题,不是应该由框架抽象并统一的行为。
-
第二步:通过计算缓存把当前对象id转化为计算值
上面的代码会得到一系列聚合根对象,如果数据库采用官方例子的数据,会得到两个聚合根对象。
代码中的对象抓取器包含了计算属性
BookStore.avgPrice
这2条BOOK_STORE的主键
ID
为1和2。Jimmer先从Redis查找数据,被查找的键为
BookStore.avgPrice-1
和BookStore.avgPrice-2
。假设无法在Redis中找到这些键所对应的数据
127.0.0.1:6379> keys BookStore.avgPrice-*
(empty array)所以,执行如下SQL,完成计算属性的计算
select
tb_1_.ID,
avg(tb_2_.PRICE)
from BOOK_STORE tb_1_
left join BOOK tb_2_
on tb_1_.ID = tb_2_.STORE_ID
where
tb_1_.ID in (
? /* 1 */, ? /* 2 */
)
group by
tb_1_.IDJimmer会把查询结果放入Redis,因此,我们可以从redis中查看这些数据
127.0.0.1:6379> keys BookStore.avgPrice-*
1) "BookStore.avgPrice-2"
2) "BookStore.avgPrice-1"
127.0.0.1:6379> get BookStore.avgPrice-1
"58.5"
127.0.0.1:6379> get BookStore.avgPrice-2
"80.333333"
127.0.0.1:6379>这样,两个
BookStore
对象可以通过其计算属性BookStore.avgPrice
得到各自书籍的平均价格。毫无疑问,在Redis中的数据因超时而被清除之前,再次执行上述Java/Kotlin代码,将直接从Redis中返回计算数据,第二条SQL不会被生成。
最终,Jimmer把3个步骤的结果拼接在一起,作为最终返回给用户的数据
[
{
"id":2,
"name":"MANNING",
"website":null,
"avgPrice":58.5
},
{
"id":1,
"name":"O'REILLY",
"website":null,
"avgPrice":80.333333
}
]
一致性
响应触发器
和对象缓存、关联缓存那种全自动的缓存一致性不同,计算缓存的一致性维护需要用户辅助。
这是因为计算属性引入了ORM框架无法理解的用户自定义计算规则
就BookStore.avgPrice
这个计算属性而言,以下两种情况都会导致计算缓存的失效
-
修改
BOOK
记录的STORE_ID
外键字段,新旧值对应的两个书店的avgPrice
缓存数据都需要被删除 -
修改
BOOK
记录的PRICE
字段,其所属书店的avgPrice
缓存数据需要被删除
在复杂计算属性一文中,为支持计算属性BookStore.avgPrice
定义了一个类BookStoreAvgPriceResolver
,其代码如下。
- Java
- Kotlin
package com.example.business.resolver;
import org.babyfish.jimmer.sql.*;
import org.babyfish.jimmer.sql.TransientResolver;
import org.springframework.stereotype.Component;
@Component
public class BookStoreAvgPriceResolver implements TransientResolver<Long, BigDecimal> {
@Override
public Map<Long, BigDecimal> resolve(Collection<Long> ids) {
...省略代码...
}
@Override
public BigDecimal getDefaultValue() {
...省略代码...
}
}
package com.example.business.resolver
import org.babyfish.jimmer.sql.*
import org.babyfish.jimmer.sql.kt.KTransientResolver
import org.springframework.stereotype.Component
@Component
class BookStoreAvgPriceResolver(
...略...
) : KTransientResolver<Long, BigDecimal> {
override fun resolve(ids: Collection<Long>): Map<Long, BigDecimal> {
...省略代码...
}
override fun getDefaultValue(): BigDecimal =
...省略代码...
}
我们需要修改此类,覆盖如下两个方法
- Java
- Kotlin
package com.example.business.resolver;
import org.babyfish.jimmer.sql.*;
import org.babyfish.jimmer.sql.TransientResolver;
import org.springframework.stereotype.Component;
@Component
public class BookStoreAvgPriceResolver implements TransientResolver<Long, BigDecimal> {
// 构造注入sqlClient
private final JSqlClient sqlClient;
...省略其他代码...
@Override
Collection<?> getAffectedSourceIds(@NotNull EntityEvent<?> e) {
// TODO
}
@Override
Collection<?> getAffectedSourceIds(@NotNull AssociationEvent e) {
// TODO
}
}
package com.example.business.resolver
import org.babyfish.jimmer.sql.*
import org.babyfish.jimmer.sql.kt.KTransientResolver
import org.springframework.stereotype.Component
@Component
class BookStoreAvgPriceResolver(
// 构造注入sqlClient
private val sqlClient KSqlClient
) : KTransientResolver<Long, BigDecimal> {
...省略其他代码...
override fun getAffectedSourceIds(e: EntityEvent<?>): Collection<*>? {
// TODO
}
override fun getAffectedSourceIds(e: AssociationEvent): Collection<*>? {
// TODO
}
}
这两个方法是TransientResolver
内置的触发器响应方法,在数据库变化时被自动执行,负责在数据库变化时自动清理计算缓存。
接下来,我们实现这两个方法。
当外键BOOK.STORE_ID被修改时
用户可以通过修改BOOK
表的外键STORE_ID
来改变BOOK_STORE
和BOOK
之间的关联,这必然会影响某些书店的BookStore.avgPrice
。
如果关注多对一关联Book.store
的变化,那么修改前的旧值和修改后的新值就是两个父对象,要分别考虑它们是否为null,代码会稍微繁琐一点。
幸运的是,本例所基于的实体模型中,存在逆向的一对多关联BookStore.books
。如果监听一对多关联BookStore.books
的变化,我们只需要考虑当前BookStore
对象的id即可,代码会得到简化。
实现getAffectedSourceIds(AssociationEvent)
,如下
- Java
- Kotlin
@Override
public Collection<?> getAffectedSourceIds(AssociationEvent e) {
if (sqlClient.getCaches().isAffectedBy(e) && ❶
e.getImmutableProp() == BookStoreProps.BOOKS.unwrap() ❷
) {
return Collections.singletonList(e.getSourceId()); ❸
}
return null;
}
override fun getAffectedSourceIds(e: AssociationEvent): Collection<*> {
if (sqlClient.caches.isAffectedBy(e) && ❶
e.immutableProp == BookProps.BOOKS ❷
) {
return listOf(e.sourceId) ❸
}
return null
}
-
❶ 如果触发器的类型被设置为
BOTH
,任何修改导致的触发器事件通知都会被执行 两次。备注第1次:
e.connection
非null,表示这是Transaction触发器发出的通知。第2次:
e.connection
为null,表示这是BinLog触发器发出的通知。然而,计算缓存的一致性维护工作只需要做一次,无需做两次。
sqlClient.caches.isAffectedBy(e)
可以解决这个问题,即使触发器的类型被设置为BOTH
,也可保证这里的代码只会执行一次。警告无论Jimmer的触发器的类型是否被设置为
BOTH
,都建议将此检查作为一个纪律性行为。 -
❷ 如果一对多关联
BookStore.books
被修改 -
❹ 那么,该关联修改事件的
sourceId
(即,BookStore
的id) 所对应的计算属性BookStore.avgPrice
的缓存需要被清理。
现在,让我们来验证修改Book.store
的效果
-
假如启用了BinLog触发器,用任何手段修改数据库都可以导致Jimmer缓存一致性的介入。比如直接在SQL IDE中执行如下SQL
update BOOK
/* Old value: 1, New value: 2 */
set STORE_ID = 2
where ID = 7;