Kotlin挂起函数结合Spring Boot+Caffeine缓存重复添加问题排查
Kotlin Suspend函数缓存重复添加问题分析与解决
问题原因
- 协程并发无互斥保护:手动实现缓存时,缓存检查与数据加载、写入操作并非原子性的。多个协程并发请求同一个未缓存的Key时,都会绕过缓存检查进入从Cookie解析购物车的逻辑,最终导致相同Key的重复缓存写入。而
@Cacheable(sync=true)内部会为同一Key的并发请求提供互斥机制,仅允许一个协程执行数据加载,其余协程等待缓存写入完成后直接读取结果。 - 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
相关产品推荐
相关产品推荐

