Kotlin协程并发场景下缓存层的重复请求规避方案咨询
解决协程下并发请求时的缓存层重复网络请求问题
好问题!你当前的实现确实存在并发缓存击穿的风险——当多个协程同时调用getItems()时,由于cachedItems还未被初始化,所有协程都会绕过缓存检查,发起重复的网络请求。用Kotlin协程处理这类场景,有两个非常实用的方案,我优先推荐第二种:
方案一:使用Mutex加锁保护缓存检查与初始化
Mutex是协程中的互斥锁,它能确保同一时间只有一个协程执行临界区代码,避免重复请求。我们可以用它包裹缓存初始化的逻辑:
class Repository(private val webServices: WebServices) { private var cachedItems: List<Item>? = null private val mutex = Mutex() // 协程安全的互斥锁 suspend fun getItems(): List<Item> { // 先尝试直接返回缓存,避免每次都加锁 cachedItems?.let { return it } // 加锁处理缓存初始化 return mutex.withLock { // 二次检查!防止等待锁的过程中已有其他协程完成了缓存初始化 cachedItems?.let { return it } val items = withContext(Dispatchers.IO) { webServices.getItems() } cachedItems = items items } } }
这个方案逻辑直观,和传统多线程的锁思路一致,但每次缓存为空时都会有协程等待锁,适合缓存频繁失效的场景。
方案二:缓存Deferred对象(推荐)
更优雅的方式是缓存网络请求的Deferred对象,而不是最终的数据结果。Deferred本身就代表了一个异步任务,多个协程可以同时await()它的结果,且只会触发一次实际执行:
class Repository(private val webServices: WebServices) { private var cachedRequest: Deferred<List<Item>>? = null private val mutex = Mutex() // 保护cachedRequest的并发修改 suspend fun getItems(): List<Item> { // 先尝试复用已有的请求 cachedRequest?.let { return it.await() } return mutex.withLock { // 二次检查,避免竞态条件 cachedRequest?.let { return it.await() } // 创建异步请求并缓存 val request = CoroutineScope(Dispatchers.IO).async { webServices.getItems() } cachedRequest = request request.await() } } // 如果需要刷新缓存,只需清空cachedRequest即可 suspend fun invalidateCache() { mutex.withLock { cachedRequest = null } } }
为什么推荐这个方案?
- 性能更优:只有第一次请求会触发网络调用,后续所有协程都直接等待同一个异步任务的结果,无需重复加锁等待缓存初始化。
- 天然适配协程模型:
Deferred是协程原生的异步任务抽象,比锁更贴合协程的非阻塞特性。 - 缓存失效简单:如果需要刷新数据,只需将
cachedRequest置为null,下一次调用getItems()就会重新发起请求。
额外注意事项
- 如果你使用的是ViewModel或其他有生命周期的组件,要注意协程上下文的绑定,避免内存泄漏(比如可以用
viewModelScope代替全局CoroutineScope)。 - 如果你的缓存需要设置过期时间,可以结合
flow和delay实现自动失效逻辑,不过这属于进阶场景了。
内容的提问来源于stack exchange,提问作者Chris Horner
相关产品推荐
相关产品推荐

