跳到主要内容

4.9 后台任务、缓存与健康检查

后台任务负责处理不适合卡在 HTTP 请求里的工作,缓存负责减少重复计算和重复访问外部依赖,健康检查负责告诉平台“应用现在能不能接流量”。三者都属于生产应用的基础能力,但都需要明确边界,不能用来掩盖业务流程设计问题。

学习目标

  • 能使用 BackgroundService 编写周期性后台任务。
  • 能区分内存缓存和分布式缓存的适用场景。
  • 能为 API 配置存活检查和就绪检查。
  • 能识别后台任务、缓存和健康检查中的常见生产风险。

应用场景

  • 定时扫描逾期任务并发送提醒。
  • 缓存首页统计、权限字典或外部配置,降低数据库压力。
  • 部署后让负载均衡器判断实例是否可以接收请求。
  • 后台同步第三方系统状态,避免用户请求长时间等待。

核心概念

概念说明
IHostedServiceASP.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
  • 思考多实例部署时,后台提醒任务如何避免重复发送。

延伸阅读