【问题标题】:spring boot cachable, ehcache with Kotlin coroutines - best practisesspring boot cachable, ehcache with Kotlin coroutines - 最佳实践
【发布时间】:2022-11-17 20:22:57
【问题描述】:

我正在努力使用 spring boot @Cacheable 和 ehcache 在两种方法上正确使用协程来处理缓存:

  1. 使用 webclient 调用另一个服务:
    suspend fun getDeviceOwner(correlationId: String, ownerId: String): DeviceOwner{
        webClient
                    .get()
                    .uri(uriProvider.provideUrl())
                    .header(CORRELATION_ID, correlationId)
                    .retrieve()
                    .onStatus(HttpStatus::isError) {response ->
                        Mono.error(
                            ServiceCallExcpetion("Call failed with: ${response.statusCode()}")
                        )
                    }.awaitBodyOrNull()
                    ?: throw ServiceCallExcpetion("Call failed with - response is null.")
    }
    
    1. 使用 r2dbc 调用数据库
    
    suspend fun findDeviceTokens(ownerId: UUID, deviceType: String) {
      //CoroutineCrudRepository.findTokens
    }
    

    似乎对我有用的是来自:

    suspend fun findTokens(data: Data): Collection<String> = coroutineScope {
            val ownership = async(Dispatchers.IO, CoroutineStart.LAZY) { service.getDeviceOwner(data.nonce, data.ownerId) }.await()
            val tokens = async(Dispatchers.IO, CoroutineStart.LAZY) {service.findDeviceTokens(ownership.ownerId, ownership.ownershipType)}
            tokens.await()
        }
    
        @Cacheable(value = ["ownerCache"], key = "#ownerId")
    fun getDeviceOwner(correlationId: String, ownerId: String)= runBlocking(Dispatchers.IO) {
        //webClientCall
    }
    
     @Cacheable("deviceCache")
    override fun findDeviceTokens(ownerId: UUID, deviceType: String) = runBlocking(Dispatchers.IO) {
      //CoroutineCrudRepository.findTokens
    }
    

    但是从我正在阅读的内容来看,使用 runBlocking 并不是一个好习惯。 https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine 它会阻塞主线程还是父协程指定的线程?

    我也试过

        @Cacheable(value = ["ownerCache"], key = "#ownerId")
    fun getDeviceOwnerAsync(correlationId: String, ownerId: String) = GlobalScope.async(Dispatchers.IO, CoroutineStart.LAZY) {
        //webClientCall
    }
    
     @Cacheable("deviceCache")
    override fun findDeviceTokensAsync(ownerId: UUID, deviceType: String) = GlobalScope.async(Dispatchers.IO, CoroutineStart.LAZY) {
      //CoroutineCrudRepository.findTokens
    }
    

    两者都是从挂起的函数中调用的,没有任何额外的 coroutineScope {} 和 async{}

    suspend fun findTokens(data: Data): Collection<String> =
        service.getDeviceOwnerAsync(data.nonce,data.ownerId).await()
           .let{service.findDeviceTokensAsync(it.ownerId, it.ownershipType).await()}
        
    

    我读到使用 GlobalScope 不是一个好习惯,因为当某些东西卡住或长时间响应时可能会无休止地运行这个协程(用非常简单的话)。同样在这种方法中,使用 GlobalScope,当我测试负面场景并且外部 ms 调用导致 404(故意)时,结果没有存储在缓存中(正如我所除外),但是对于失败的 CoroutineCrudRepository.findTokens 调用(抛出异常)延迟值是缓存这不是我想要的。存储失败的执行结果与 runBlocking 无关。

    我也试过@Cacheable("deviceCache", unless = "#result.isCompleted == true &amp;&amp; #result.isCancelled == true") 但它似乎也不像我想象的那样有效。

    您能否建议最好的协程方法和正确的异常处理以与 spring boot 缓存集成,它只会在非失败调用时将值存储在缓存中?

【问题讨论】:

    标签: spring-boot caching kotlin-coroutines


    【解决方案1】:

    尽管来自 Spring Cache abstraction 的注释很花哨,但不幸的是,我还没有找到任何官方解决方案来将它们与 Kotlin 协程一起使用。

    然而,有一个名为spring-kotlin-coroutine 的库声称可以解决这个问题。不过,从未尝试过,因为它似乎不再维护了——最后一次提交是在 2019 年 5 月推出的。

    目前我一直在使用CacheManager bean 并手动管理上述内容。我发现一个更好的解决方案而不是阻塞线程。


    使用 Redis 作为缓存提供程序的示例代码:

    build.gradle.kts 中的依赖:

    implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
    

    application.yml配置:

    spring:
      redis:
        host: redis
        port: 6379
        password: changeit
      cache:
        type: REDIS
        cache-names:
          - account-exists
        redis:
          time-to-live: 3m
     
    

    代码:

    @Service
    class AccountService(
      private val accountServiceApiClient: AccountServiceApiClient,
      private val redisCacheManager: RedisCacheManager
    ) {
    
      suspend fun isAccountExisting(accountId: UUID): Boolean {
        if (getAccountExistsCache().get(accountId)?.get() as Boolean? == true) {
          return true
        }
        
        val account = accountServiceApiClient.getAccountById(accountId) // this call is reactive
        if (account != null) {
          getAccountExistsCache().put(account.id, true)
          return true
        }
        
        return false
      }
      
      private fun getAccountExistsCache() = redisCacheManager.getCache("account-exists") as RedisCache
    }
    

    【讨论】:

      【解决方案2】:

      在 Kotlin Coroutines 上下文中,每个 suspend 函数都有 1 个 kotlin.coroutines.Continuation&lt;T&gt; 类型的额外参数,这就是为什么 org.springframework.cache.interceptor.SimpleKeyGenerator 总是生成错误的 key。此外,CacheInterceptor 对 suspend 函数一无所知,因此它存储一个 COROUTINE_SUSPENDED 对象而不是实际值,而不评估挂起的包装器。

      你可以查看这个仓库https://github.com/konrad-kaminski/spring-kotlin-coroutine,他们添加了对Coroutines的Cache支持,具体的Cache支持实现在这里 -> https://github.com/konrad-kaminski/spring-kotlin-coroutine/blob/master/spring-kotlin-coroutine/src/main/kotlin/org/springframework/kotlin/coroutine/cache/CoroutineCacheConfiguration.kt

      看看CoroutineCacheInterceptorCoroutineAwareSimpleKeyGenerator

      希望这能解决您的问题

      【讨论】:

        猜你喜欢
        • 2015-04-19
        • 2020-03-08
        • 1970-01-01
        • 2020-09-30
        • 2020-04-19
        • 1970-01-01
        • 2022-01-07
        • 2023-03-18
        • 2019-02-05
        相关资源
        最近更新 更多