大多数API都遵循一个简单的模式。客户端发送请求。服务器处理一些请求。服务器返回响应。
这在快速操作,比如获取数据或简单更新中效果很好。但耗时较长的操作又该怎么办呢?
想想处理大型文件、生成报告或转换视频这些任务,可能需要几分钟到几小时。
让客户等待这些操作会导致问题。
理解异步API处理长时间运行任务的关键在于改变我们看待API响应的方式。一个异步API将任务分为两部分:
- 接受请求、
- 稍后再处理它、
首先,我们立即接受请求并返回一个跟踪ID,这为用户提供了快速的反馈。接着,我们在后台处理实际任务,确保不会影响其他请求的处理。用户可以随时通过跟踪ID来查看请求的状态。
这不同于 C# 中的 async
/await
。它主要处理的是同时(并发)处理多个请求。这里重点是更好地处理长时间的任务。我们不只是让代码异步,而是让用户感觉整个操作都是异步的。
让我们实际看看这个在图像处理中的应用。一个典型的图像上传 API 可能看起来像下面这样:
[HttpPost]
public async Task<IActionResult> UploadImage(IFormFile file)
{
if (file is null)
{
return BadRequest();
}
// 保存原图文件
var originalPath = await SaveOriginalAsync(file);
// 生成缩略图:
var thumbnails = await GenerateThumbnailsAsync(originalPath);
// 优化所有图像
await OptimizeImagesAsync(originalPath, thumbnails);
return Ok(new { originalPath, thumbnails });
}
客户需要等待我们保存文件到服务器、生成缩略图并优化图片。如果连接速度慢或文件很大,此请求可能会超时。服务器也是一次只能处理一张图片,效率较低。请稍等片刻。
更好的方法:异步处理咱们来解决这些问题吧。分成两部分来处理。
- 接收上传并迅速响应
- 在后台做繁重的工作
上传图片
这是新的上传接口:
[HttpPost]
public async Task<IActionResult> UploadImage(IFormFile? file)
{
if (file is null)
{
return BadRequest("没有上传文件。");
}
if (!imageService.IsValidImage(file))
{
return BadRequest("无效的图片文件。");
}
// 第一阶段:接收工作
var id = Guid.NewGuid().ToString();
var folderPath = Path.Combine(_uploadDirectory, "images", id);
var fileName = $"{id}{Path.GetExtension(file.FileName)}";
var originalPath = await imageService.SaveOriginalImageAsync(
file,
folderPath,
fileName
);
// 将第二阶段的任务加入后台处理队列
var job = new ImageProcessingJob(id, originalPath, folderPath);
await jobQueue.EnqueueAsync(job);
// 立即返回状态URL以获取任务状态
var statusUrl = GetStatusUrl(id);
return Accepted(statusUrl, new { id, status = "已排队" });
}
新版本只在HTTP请求期间保存原始文件。繁重的工作则在后台进程中进行。客户端不再等待,而是立即从Location
头部获取状态URL。
客户可以使用单独的端点来检查其图像的状态。
[HttpGet("{id}/status")]
public IActionResult GetStatus(string id)
{
if (!状态追踪器.TryGetStatus(id, out var status))
{
return NotFound();
}
var response = new
{
id,
status,
links = new Dictionary<string, string>()
};
if (status == "completed")
{
response.links = new Dictionary<string, string>
{
["original"] = 获取图片URL(id: id),
["thumbnail"] = 获取缩略图URL(id, 宽度: 200),
["preview"] = 获取缩略图URL(id, 宽度: 800)
};
}
return Ok(response);
}
后台处理图像
真正的处理工作是在后台任务处理器中进行的。当API接收新的请求时,一个独立的进程处理排队的任务。这种分离使我们在处理上更加灵活。
在单服务器部署的情况下,我们可以利用 .NET 的 Channel 在内存中排队作业。
public class JobQueue
{
private readonly Channel<ImageProcessingJob> _channel;
public JobQueue()
{
var options = new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait
};
_channel = Channel.CreateBounded<ImageProcessingJob>(options);
}
public async ValueTask EnqueueAsync(ImageProcessingJob job,
CancellationToken ct = default)
{
await _channel.Writer.WriteAsync(job, ct);
}
public IAsyncEnumerable<ImageProcessingJob> DequeueAsync(
CancellationToken ct = default)
{
return _channel.Reader.ReadAllAsync(ct);
}
}
对于多服务器架构,我们需要像 RabbitMQ 或甚至 Redis 这样的工具,作为分布式队列系统。
后台处理程序负责处理那些耗时的任务:
public class ImageProcessor : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var job in jobQueue.DequeueAsync(ct))
{
try
{
await statusTracker.SetStatusAsync(
job.Id,
"processing"
);
// 生成缩略图
await GenerateThumbnailsAsync(
job.OriginalPath,
job.OutputPath
);
// 优化图像
await OptimizeImagesAsync(
job.OriginalPath,
job.OutputPath
);
await statusTracker.SetStatusAsync(
job.Id,
"已完成"
);
}
catch (Exception ex)
{
await statusTracker.SetStatusAsync(
job.Id,
"失败"
);
logger.LogError(ex, "处理图像 {Id} 时失败", job.Id);
}
}
}
}
后台处理器需要优雅地处理失败情况。我们可以通过添加Polly的重试策略来提高[弹性](弹性指系统在面对故障时恢复的能力)。状态更新在整个过程中让用户了解情况。我们不仅告诉用户“正在处理”,还会详细告知他们具体发生了哪些事情。这不仅改善了用户体验,也有助于调试。
超越投票:实时信息更新我们的状态端点是工作的,但这给客户端带来了负担。他们不断检查更新,从而导致不必要的服务器负载。每秒轮询检查一次的客户端会产生每分钟60个请求,然而其中大多数请求返回的状态都是一样的。
我们可以反过来这个模型。不再是客户请求更新,而是服务器在有更新时主动推送出去。这样可以构建一个更高效且响应更快的系统。
SignalR 和 WebSockets 让服务器与客户端之间能够实现实时通信。当工作的状态发生变化时,服务器会立即通知感兴趣的客户端。这种方法不仅减少了网络流量,还为用户提供即时反馈。
对于长时间运行的任务,电子邮件通知更有意义。用户不需要一直开着浏览器,他们可以关闭标签页,在收到通知时再回来。这适用于需要数小时生成的报告或夜间运行的批处理任务。
Webhooks 提供了另一种选择,特别适用于系统间通信。当一个任务完成后,这时你的服务器可以通知其他系统。这使得工作流程能够自动化。同时,系统整合也不再需要持续轮询。
摘要异步处理任务能为所有人提供更好的体验。用户能立即得到回应,无需盯着那个转圈的加载指示器。他们还可以在等待时做其他事情,并且如果出错,他们也会被告知。
好处不仅仅体现在用户体验上。服务器能够处理更多的请求,因为它们不会被长时间的任务拖累。后台任务处理器 可以在不影响主程序的情况下重试失败的任务。您甚至可以独立于Web服务器来扩展处理量。
错误处理在 ASP.NET Core 8 中 也得到了改进。当一个长时间的操作在中途失败时,可以保存进度,并在稍后重新尝试。用户可以随时查看状态,因此他们确切地知道发生了什么。系统能够保持稳定,因为一个缓慢的操作不会拖垮整个 API。
今天的分享就到这里了。希望这些对你有用。
原文发布于在https://www.milanjovanovic.tech上,2024年11月23日,_
无论你什么时候准备好,我可以从这四个方面帮助你
- (即将推出) 使用 ASP.NET Core 构建 REST API: 您将学习如何使用 ASP.NET Core 的最新功能和最佳实践构建可投入生产的 REST API。该课程包含一个完全功能的 UI 应用程序,我们将与 REST API 集成。加入等候列表!
- 实用的干净架构: 加入 3,150 多名学生,学习我用来构建可投入生产的 Clean Architecture 应用程序的方法。了解现代软件架构的最佳实践。
- 模块化单体架构 (模块化单体架构): 加入 1,050 多名工程师,学习如何在现实场景中应用模块化单体架构的最佳实践。这门深入的课程将教会您如何在现实场景中应用模块化单体架构的最佳实践。
- Patreon 社区: 加入一个由 1,000 多名工程师和软件架构师组成的社区。您还将获得我在 YouTube 视频中使用的源代码的访问权限以及未来视频的抢先体验和我的课程独家折扣。
共同学习,写下你的评论
评论加载中...
作者其他优质文章