4.9 后台任务、缓存与健康检查
后台任务负责处理不适合卡在 HTTP 请求里的工作,缓存负责减少重复计算和重复访问外部依赖,健康检查负责告诉平台“应用现在能不能接流量”。三者都属于生产应用的基础能力,但都需要明确边界,不能用来掩盖业务流程设计问题。
学习目标
- 能使用
BackgroundService编写周期性后台任务。 - 能区分内存缓存和分布式缓存的适用场景。
- 能为 API 配置存活检查和就绪检查。
- 能识别后台任务、缓存和健康检查中的常见生产风险。
应用场景
- 定时扫描逾期任务并发送提醒。
- 缓存首页统计、权限字典或外部配置,降低数据库压力。
- 部署后让负载均衡器判断实例是否可以接收请求。
- 后台同步第三方系统状态,避免用户请求长时间等待。
核心概念
| 概念 | 说明 |
|---|---|
IHostedService | ASP.NET Core 宿主启动和停止时管理的后台服务接口 |
BackgroundService | 基于 IHostedService 的常用基类,适合长期运行任务 |
IMemoryCache | 单进程内存缓存,简单快速,但多实例之间不共享 |
IDistributedCache | 分布式缓存抽象,可接 Redis、SQL Server 等后端 |
| Liveness | 存活检查,回答进程是否还活着 |
| Readiness | 就绪检查,回答实例是否可以接收真实流量 |
案例:逾期任务提醒后台服务
后台服务应通过 DI 创建作用域,再解析 Scoped 服务,例如 DbContext 或应用服务。不要把 Scoped 服务直接注入长期存活的 Singleton 后台服务中。
public sealed class OverdueTodoReminderWorker(
IServiceScopeFactory scopeFactory,
ILogger<OverdueTodoReminderWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await SendRemindersAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send overdue todo reminders");
}
}
}
private async Task SendRemindersAsync(CancellationToken cancellationToken)
{
await using var scope = scopeFactory.CreateAsyncScope();
var reminderService = scope.ServiceProvider.GetRequiredService<TodoReminderService>();
var count = await reminderService.SendOverdueRemindersAsync(cancellationToken);
logger.LogInformation("Sent {ReminderCount} overdue todo reminders", count);
}
}
注册后台服务和业务服务:
builder.Services.AddScoped<TodoReminderService>();
builder.Services.AddHostedService<OverdueTodoReminderWorker>();
后台任务要支持取消令牌,应用停止时才能优雅退出。生产环境还要考虑任务幂等、失败重试、并发实例重复执行和任务执行时间超过间隔的问题。
示例:内存缓存
IMemoryCache 适合缓存成本较低、允许短暂不一致、单实例可接受的数据,例如首页统计摘要。
public sealed class TodoSummaryService(
AppDbContext dbContext,
IMemoryCache cache)
{
public async Task<TodoSummaryDto> GetSummaryAsync(CancellationToken cancellationToken)
{
return await cache.GetOrCreateAsync("todo-summary", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
entry.SlidingExpiration = TimeSpan.FromSeconds(20);
var total = await dbContext.Todos.CountAsync(cancellationToken);
var completed = await dbContext.Todos.CountAsync(
todo => todo.IsCompleted,
cancellationToken);
return new TodoSummaryDto(total, completed);
}) ?? new TodoSummaryDto(0, 0);
}
}
注册缓存:
builder.Services.AddMemoryCache();
builder.Services.AddScoped<TodoSummaryService>();
缓存不是数据库。写操作完成后,要么删除相关缓存,要么接受短暂过期窗口。不要缓存包含用户敏感信息的大对象,也不要给无限增长的 Key 空间无上限缓存。
示例:分布式缓存
多实例部署时,内存缓存只在当前进程有效。需要跨实例共享缓存、会话或限流状态时,可以使用 IDistributedCache。
public sealed class PermissionCache(IDistributedCache cache)
{
public async Task CachePermissionsAsync(
string userId,
IReadOnlyCollection<string> permissions,
CancellationToken cancellationToken)
{
var json = JsonSerializer.Serialize(permissions);
await cache.SetStringAsync(
$"permissions:{userId}",
json,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
},
cancellationToken);
}
}
分布式缓存带来网络 IO 和序列化成本。只有当共享状态、跨实例一致性或缓存容量明显需要它时,再引入 Redis 等外部依赖。
示例:健康检查
存活检查通常只确认进程可响应;就绪检查应检查关键依赖,例如数据库或消息队列。
builder.Services
.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("database", tags: ["ready"]);
var app = builder.Build();
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false,
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
});
/health/live 不应检查太多外部依赖,否则数据库短暂抖动可能导致平台误杀健康进程;/health/ready 可以更严格,用来控制是否接收流量。
重点难点
- 后台任务默认和 Web 应用运行在同一个进程中,长时间 CPU 密集任务会影响 API 响应。
- 多实例部署时,同一个
BackgroundService可能在每个实例都运行,需要分布式锁、队列或单独 Worker 进程来控制重复执行。 - 缓存必须有过期策略和失效策略,不能只会写入不会删除。
- 健康检查端点不要泄露连接字符串、异常堆栈和内部拓扑。
- 对关键任务来说,消息队列通常比“定时扫库”更可靠。
常见误区
| 误区 | 推荐做法 |
|---|---|
| 在 HTTP 请求里同步等待长任务完成 | 接收请求后入队或记录任务,再由后台处理 |
后台服务直接注入 DbContext | 注入 IServiceScopeFactory,每轮任务创建作用域 |
| 给所有查询都加缓存 | 只缓存热点、可过期、可接受短暂不一致的数据 |
| 健康检查越多越好 | 区分 live 和 ready,避免依赖抖动导致错误重启 |
| 缓存命中后不再考虑权限 | 权限仍要按当前用户和资源重新校验 |
练习
- 写一个每 10 分钟清理过期 Todo 草稿的
BackgroundService。 - 给任务统计接口增加 60 秒内存缓存,并在创建任务后删除缓存。
- 为数据库连接配置
/health/ready,为进程存活配置/health/live。 - 思考多实例部署时,后台提醒任务如何避免重复发送。
延伸阅读
- 本手册:依赖注入与中间件
- 本手册:配置、Options 与日志
- 本手册:可观测性与健康检查