You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin挂起函数结合Spring Boot+Caffeine缓存重复添加问题排查

Kotlin Suspend函数缓存重复添加问题分析与解决

问题原因

  1. 协程并发无互斥保护:手动实现缓存时,缓存检查与数据加载、写入操作并非原子性的。多个协程并发请求同一个未缓存的Key时,都会绕过缓存检查进入从Cookie解析购物车的逻辑,最终导致相同Key的重复缓存写入。而@Cacheable(sync=true)内部会为同一Key的并发请求提供互斥机制,仅允许一个协程执行数据加载,其余协程等待缓存写入完成后直接读取结果。
  2. Suspend函数非阻塞特性导致的竞态:挂起函数不会阻塞线程,多个协程可在同一线程上并发调度执行。若手动缓存逻辑未做线程安全的并发控制,就会出现多个协程同时执行缓存写入操作的竞态场景,进而产生重复缓存项。

解决办法

方案1:使用Spring原生协程缓存支持(推荐)

Spring 5.3及以上版本已原生支持Kotlin suspend函数与@Cacheable注解结合,无需手动实现缓存逻辑:

  • 确保Spring Boot版本≥2.4(对应Spring 5.3)
  • 在suspend函数上直接标注@Cacheable,配合Caffeine配置即可,示例代码:
@Cacheable(value = ["cartCache"], key = "#cartId", sync = true)
suspend fun getCartFromCookie(cartId: String): Cart {
    // 从Cookie解析购物车的业务逻辑
    return parseCartFromCookie(cartId)
}

注意:sync=true仍需保留,用于控制并发场景下的缓存加载互斥,避免重复加载。

方案2:手动实现缓存时添加协程友好的并发控制

若坚持手动实现缓存,可使用Kotlin协程的Mutex实现同一Key的加载互斥,配合双重检查锁定避免竞态:

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import com.github.benmanes.caffeine.cache.Caffeine
import java.util.concurrent.ConcurrentHashMap

private val cartCache = Caffeine.newBuilder().build<String, Cart>()
private val keyMutexes = ConcurrentHashMap<String, Mutex>()

suspend fun getCart(cartId: String): Cart {
    // 首次缓存检查
    cartCache.getIfPresent(cartId)?.let { return it }
    
    // 获取对应Key的互斥锁,不存在则创建
    val mutex = keyMutexes.computeIfAbsent(cartId) { Mutex() }
    
    return mutex.withLock {
        // 加锁后再次检查缓存,避免等待期间已被其他协程写入
        cartCache.getIfPresent(cartId) ?: run {
            val cart = parseCartFromCookie(cartId)
            cartCache.put(cartId, cart)
            // 移除锁避免内存泄漏,也可改用弱引用Map存储锁
            keyMutexes.remove(cartId)
            cart
        }
    }
}

这里的核心是双重检查锁定,确保在协程等待锁的过程中,其他协程已完成缓存写入时,直接读取缓存而非重复加载。Mutex是协程友好的锁,不会阻塞线程,适配suspend函数的非阻塞特性。

方案3:沿用Deferred配合@Cacheable(sync=true)的可行方案

你当前使用的Deferred结合@Cacheable(sync=true)的模式可继续沿用,本质是通过Deferred让多个协程共享同一加载任务的结果,避免重复执行解析逻辑:

import kotlinx.coroutines.Deferred
import kotlinx.coroutines.GlobalScope.async
import org.springframework.cache.annotation.Cacheable

@Cacheable(value = ["cartCache"], key = "#cartId", sync = true)
suspend fun getCartDeferred(cartId: String): Deferred<Cart> {
    return GlobalScope.async {
        parseCartFromCookie(cartId)
    }
}

// 业务调用方
suspend fun getCart(cartId: String): Cart {
    return getCartDeferred(cartId).await()
}

内容的提问来源于stack exchange,提问作者Shaunyl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 06:15:28