ASP.NET Core中按参数处理API请求:同ID等待缓存填充
最优实现方案
针对多Docker容器集群下的缓存填充并发控制需求,核心是通过分布式锁+共享状态标记的组合,避免相同ID的请求重复触发耗时的缓存填充,同时让后续请求等待填充完成后复用结果。具体实现步骤如下:
核心思路
- 优先从Blob缓存读取数据,命中直接返回。
- 缓存未命中时,通过分布式锁确保同一时间只有一个请求进入缓存填充流程。
- 第一个请求进入填充流程后,标记该ID的"填充中"状态,释放锁以避免后续请求阻塞在锁竞争上。
- 后续相同ID的请求发现"填充中"状态后,等待状态清除,再读取Blob缓存返回结果。
- 填充完成后写入Blob缓存,并清除"填充中"状态。
技术选型与实现细节
1. 分布式锁实现
因为是多容器集群,必须用跨实例的分布式锁:
- 若使用Azure Blob存储,可利用Blob租约实现锁:为对应ID的Blob创建独占租约,有效期设为填充操作的最大预估时间(如5分钟),租约到期自动释放,避免异常导致死锁。
- 通用场景可使用Redis的分布式锁(如
IDistributedLock组件),锁粒度为单个ID(锁键格式如report-lock:{id}),确保仅针对当前ID的请求互斥。
2. 填充状态标记
用分布式存储维护填充状态,确保所有容器实例可见:
- 推荐用Redis字符串键(格式如
report-pending:{id}),设置合理过期时间(如5分钟),防止填充过程中服务崩溃导致状态永久留存。 - 若依赖现有Blob存储,可创建临时文件(如
{id}-pending),填充完成后删除该文件作为状态清除标记。
3. 请求等待机制
两种可选方式:
- 轮询检查:后续请求每隔固定间隔(如500ms)检查状态,直到状态消失或超时(如30s),超时则返回降级响应。
- Pub/Sub通知:第一个请求填充完成后,通过Redis发布订阅机制发送通知,后续请求订阅该ID的通知,收到后立即读取缓存,效率更高。
示例代码(Redis实现)
public async Task<ActionResult> GetReport(Guid id) { // 1. 尝试从Blob缓存读取 var report = await _blobCache.GetReportAsync(id); if (report != null) return Ok(report); var lockKey = $"report-lock:{id}"; var pendingKey = $"report-pending:{id}"; // 2. 获取分布式锁,尝试时间30s using var lockHandle = await _distributedLock.TryAcquireLockAsync(lockKey, TimeSpan.FromSeconds(30)); if (lockHandle != null) { // 3. 二次检查缓存,避免等待锁期间已被其他请求填充 report = await _blobCache.GetReportAsync(id); if (report != null) return Ok(report); // 4. 设置填充中状态,5分钟过期 await _redisCache.StringSetAsync(pendingKey, "true", TimeSpan.FromMinutes(5)); // 5. 提前释放锁,减少后续请求的锁竞争 lockHandle.Dispose(); try { // 6. 执行耗时的报告生成与缓存写入 report = await GenerateReportAsync(id); await _blobCache.SaveReportAsync(id, report); } finally { // 7. 清除填充状态 await _redisCache.KeyDeleteAsync(pendingKey); // 发布缓存就绪通知(若用Pub/Sub) await _redisPublisher.PublishAsync($"report-ready:{id}", string.Empty); } return Ok(report); } else { // 未抢到锁,进入等待逻辑 var timeout = TimeSpan.FromSeconds(30); var startTime = DateTime.UtcNow; while (DateTime.UtcNow - startTime < timeout) { if (!await _redisCache.KeyExistsAsync(pendingKey)) { report = await _blobCache.GetReportAsync(id); if (report != null) return Ok(report); } await Task.Delay(500); } // 超时返回降级响应 return StatusCode(StatusCodes.Status503ServiceUnavailable, "报告生成超时,请稍后重试"); } }
关键注意事项
- 必须设置状态的过期时间,避免服务崩溃导致后续请求无限等待。
- 填充操作的重试逻辑要谨慎,避免重复触发填充。
- 超时时间需根据实际填充耗时调整,兼顾用户体验与资源占用。
内容的提问来源于stack exchange,提问作者cooper_milton
相关产品推荐
相关产品推荐

