Swift服务端项目:基于Actor实现原子缓存的问题与解决思路问询
基于Swift Actor实现原子性缓存的解决方案
你的思路方向是对的,但确实因为Actor的重入特性(你提到的reentrancy),异步操作会导致多个任务同时进入数据生成逻辑,进而重复计算。不过通过给Actor添加额外的状态标记,完全可以解决这个问题。
核心问题分析
你担心的异步操作破坏原子性,本质是Actor在执行await时会暂时放弃执行权,允许其他任务进入Actor的方法。如果没有状态标记,多个调用get()的任务会在缓存未命中且无生成中的情况下,都进入数据生成流程,导致重复计算和写入。
改进后的Actor实现
我们可以给CacheItem添加一个pendingTask状态,用来标记当前是否有正在进行的数据生成任务。第一个任务进入生成流程时,创建异步任务并保存引用;后续任务直接等待这个任务完成,就能避免重复工作:
actor CacheItem { private let key: String private let storage: any CacheStorage private var cachedValue: String? // 标记正在进行的生成任务,避免重复执行 private var pendingTask: Task<String, Error>? init(key: String, backingStorage: any CacheStorage) { self.key = key self.storage = backingStorage } func get() async throws -> String { // 1. 优先返回本地缓存的值 if let value = cachedValue { return value } // 2. 如果有正在进行的生成任务,直接等待其完成 if let task = pendingTask { return try await task.value } // 3. 没有缓存也没有生成任务,开始生成 let task = Task { defer { // 任务完成后清除标记,无论成功失败 pendingTask = nil } // 先尝试从后端存储读取 if let storedValue = try await storage.get(key: key) { cachedValue = storedValue return storedValue } // 生成新值(替换成你的实际异步生成逻辑) let newValue = try await generateValueForKey(key) // 写入后端存储和本地缓存 try await storage.set(key: key, value: newValue) cachedValue = newValue return newValue } pendingTask = task return try await task.value } // 模拟数据生成的异步方法 private func generateValueForKey(_ key: String) async throws -> String { // 可替换为数据库查询、API请求等异步操作 try await Task.sleep(nanoseconds: 1_000_000_000) return "generated_value_for_\(key)" } } // 定义缓存存储协议,适配Redis等后端 protocol CacheStorage { func get(key: String) async throws -> String? func set(key: String, value: String) async throws }
关键改进点
pendingTask状态标记:用Task实例跟踪正在进行的生成任务,后续请求直接复用任务结果,彻底避免重复计算。defer清除标记:确保任务无论成功还是失败,都会清除pendingTask,避免后续请求陷入无限等待。- 本地缓存+后端存储结合:本地缓存(
cachedValue)减少对后端存储的重复读取,提升整体性能。
关于Actor重入的说明
Actor的重入特性本身不是问题,只要我们通过状态标记明确当前的执行阶段,就能避免竞态条件。官方文档提到的“无await时安全更新”,是指同步代码块中修改状态不会出现中间状态被观测的情况;而通过pendingTask这种方式,我们把异步操作的状态也纳入了Actor的管理,依然能保证原子性。
替代方案:全局缓存Actor代理
如果觉得每个Key对应一个Actor开销太大,也可以用一个全局的CacheActor作为所有缓存键的互斥代理,通过字典管理每个Key的pendingTask:
actor CacheActor { private let storage: any CacheStorage private var cache: [String: String] = [:] private var pendingTasks: [String: Task<String, Error>] = [:] init(storage: any CacheStorage) { self.storage = storage } func get(key: String) async throws -> String { if let value = cache[key] { return value } if let task = pendingTasks[key] { return try await task.value } let task = Task { [key] in defer { pendingTasks[key] = nil } if let storedValue = try await storage.get(key: key) { cache[key] = storedValue return storedValue } let newValue = try await generateValueForKey(key) try await storage.set(key: key, value: newValue) cache[key] = newValue return newValue } pendingTasks[key] = task return try await task.value } private func generateValueForKey(_ key: String) async throws -> String { try await Task.sleep(nanoseconds: 1_000_000_000) return "generated_value_for_\(key)" } }
这种方案更适合缓存键数量较多的场景,避免创建大量Actor实例。
内容的提问来源于stack exchange,提问作者Jerry
相关产品推荐
相关产品推荐

