我有一个使用协程的Kotlin JVM服务器应用程序,我需要将缓存放在非阻塞网络调用的前面。我认为我可以使用咖啡因AsyncLoadingCache
来获得所需的非阻塞缓存行为。我需要实现的AsyncCacheLoader
接口使用CompletableFuture
。同时,我要调用的用于加载缓存项的方法是一个suspend
函数。
我可以这样缩小差距:
abstract class SuspendingCacheLoader<K, V>: AsyncCacheLoader<K, V> {
abstract suspend fun load(key: K): V
final override fun asyncLoad(key: K, executor: Executor): CompletableFuture<V> {
return GlobalScope.async(executor.asCoroutineDispatcher()) {
load(key)
}.asCompletableFuture()
}
}
这将在提供的load
(默认为Executor
)上运行ForkJoinPool
函数,从咖啡因的角度来看,这是正确的行为。
但是,我知道我应该尝试avoid using GlobalScope to launch coroutines。
我考虑让自己的SuspendingCacheLoader
实现CoroutineScope
并管理自己的协程上下文。但是CoroutineScope
旨在由具有托管生命周期的对象实现。缓存和AsyncCacheLoader
都没有任何生命周期挂钩。缓存拥有Executor
和CompletableFuture
实例,因此它已经以这种方式控制了加载任务的生命周期。我看不到将任务归于协程上下文会添加任何内容,而且我担心在停止使用缓存后无法正确关闭协程上下文。
编写自己的异步缓存机制非常困难,因此,如果可以的话,我想与Caffeine实现集成。
是使用GlobalScope
来实现AsyncCacheLoader
的正确方法,还是有更好的解决方案?
答案 0 :(得分:3)
缓存拥有Executor和CompletableFuture实例,因此它已经以这种方式控制了加载任务的生命周期。
这是不正确的,Caffeine
上的文档指定它使用了用户提供的Executor
或ForkJoinPool.commonPool()
(如果未提供)。这意味着没有默认生命周期。
无论直接调用GlobalScope
都是错误的解决方案,因为没有理由对选择进行硬编码。只需通过构造函数提供CoroutineScope
并使用GlobalScope
作为参数,而您没有明确的生命周期可以绑定到缓存。
答案 1 :(得分:0)
经过深思熟虑,我想出了一个不同的解决方案,该解决方案将继承用于调用get
方法的任何上下文,而不是依赖从头创建或传递给缓存加载器的固定上下文。
该方法通过使用AsyncCache.get
而不是AsyncCacheLoader
来工作。要获取当前上下文,我使用全局coroutineContext
属性。
suspend fun <K: Any, V> AsyncCache.getSuspending(key: K): V {
val outerContext = coroutineContext
return get(key) { k, executor ->
val innerContext = outerContext + Job() + executor.asCoroutineDispatcher()
CoroutineScope(innerContext).async {
loadValue(k) // loadValue is a suspend function defined elsewhere
}.asCompletableFuture()
}.await()
}
用于调用loadValue
的上下文由三件事组成:
getSuspending
Job()
,它将在外部上下文中覆盖作业。必须确保在loadValue
中引发的错误不会立即传播,而是会在CompletableFuture
中捕获,以便缓存可以适当地响应它们。executor
,因此仍然尊重缓存关于线程的配置。这确实意味着上下文对从何处调用get
很敏感,如果缓存由应用程序的不同部分共享,则可能不是正确的行为。但是,我认为这种行为比我原来的方法要好。
对此表示赞赏和评论!
答案 2 :(得分:0)
这是我的解决方法:
定义CoroutineVerticle
的扩展功能
fun <K, V> CoroutineVerticle.buildCache(configurator: Caffeine<Any, Any>.() -> Unit = {}, loader: suspend CoroutineScope.(K) -> V) = Caffeine.newBuilder().apply(configurator).buildAsync { key: K, _ ->
// do not use cache's executor
future {
loader(key)
}
}
在CoroutineVerticle
内创建我们的缓存
val cache : AsyncLoadingCache<String, String> = buildCache({
maximumSize(10_000)
expireAfterWrite(10, TimeUnit.MINUTES)
}) { key ->
// load data and return it
delay(1000)
"data for key: $key"
}
使用缓存
suspend fun doSomething() {
val data = cache.get('key').await()
val future = cache.get('key2')
val data2 = future.await()
}
答案 3 :(得分:0)
这是一个简单的解决方案。用您的类型替换K,V表示法。
val cache = Caffeine.newBuilder().buildAsync<K, V> { key: K, _ ->
val future = CompletableFuture<V>()
launch {
val result = someAwaitOperation(key)
future.complete(result)
}
future
}
答案 4 :(得分:0)
建议这样的扩展方法
suspend inline fun <K: Any, V: Any> Caffeine<Any, Any>.suspendingLoadingCache(
crossinline suspendedLoader: suspend (key: K) -> V
): AsyncLoadingCache<K, V> =
buildAsync { key, executor: Executor ->
CoroutineScope(executor.asCoroutineDispatcher()).future {
suspendedLoader(key)
}
}
不建议GlobalScope
,请使用CoroutineScope(executor.asCoroutineDispatcher())
future
方法在kotlinx-coroutines-jdk8
模块中定义