Hangfire 任务调度
大约 9 分钟约 2708 字
Hangfire 任务调度
简介
Hangfire 是 .NET 平台最流行的后台任务处理库。它提供了简单易用的 API 来创建、处理和管理后台任务,支持持久化存储(SQL Server、Redis 等),并提供内置的 Dashboard 仪表盘来监控任务状态。无需 Windows 服务或单独的进程,直接集成在 ASP.NET Core 应用中。
特点
安装与配置
NuGet 包
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer # SQL Server 存储
dotnet add package Hangfire.Redis.StackExchange # Redis 存储(可选)
dotnet add package Hangfire.Dashboard.BasicAuthorization # 认证基本配置
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. 配置存储
builder.Services.AddHangfire(config => config
.UseSqlServerStorage(builder.Configuration.GetConnectionString("Hangfire"))
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
);
// 2. 添加 Hangfire 服务
builder.Services.AddHangfireServer();
var app = builder.Build();
// 3. 启用 Dashboard(带认证)
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new BasicAuthAuthorizationFilter(
new BasicAuthAuthorizationFilterOptions
{
RequireSsl = false,
SslRedirect = false,
LoginCaseSensitive = true,
Users = new[]
{
new BasicAuthAuthorizationUser
{
Login = "admin",
PasswordClear = "admin123"
}
}
})
}
});
// 4. 启用 Hangfire Server
app.UseHangfireServer();
app.Run();// appsettings.json
{
"ConnectionStrings": {
"Hangfire": "Server=localhost;Database=HangfireDb;User Id=sa;Password=123456;"
}
}四种任务类型
1. 即时任务(Fire-and-Forget)
/// <summary>
/// 即时任务 — 放入队列立即执行,不阻塞调用方
/// 典型场景:发送邮件、生成报表、记录日志
/// </summary>
public class EmailService
{
private readonly IBackgroundJobClient _backgroundJobs;
public EmailService(IBackgroundJobClient backgroundJobs)
{
_backgroundJobs = backgroundJobs;
}
public void SendWelcomeEmail(string userEmail, string userName)
{
// 立即入队,后台执行
var jobId = _backgroundJob.Enqueue<EmailService>(
x => x.SendEmailAsync(userEmail, "欢迎注册", $"你好 {userName},欢迎加入!")
);
Console.WriteLine($"邮件任务已入队,JobId:{jobId}");
}
// 任务方法必须是 public
public async Task SendEmailAsync(string to, string subject, string body)
{
// 模拟发送邮件
await Task.Delay(2000);
Console.WriteLine($"邮件已发送:{to} - {subject}");
}
}
// 使用
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobs;
public UserController(IBackgroundJobClient backgroundJobs)
{
_backgroundJobs = backgroundJobs;
}
[HttpPost("register")]
public IActionResult Register([FromBody] RegisterRequest request)
{
// 用户注册逻辑...
var userId = SaveUser(request);
// 后台发送欢迎邮件,不阻塞用户注册响应
_backgroundJobs.Enqueue<EmailService>(
x => x.SendEmailAsync(request.Email, "欢迎注册", $"你好 {request.UserName}")
);
return Ok(new { userId });
}
}2. 延迟任务(Delayed)
/// <summary>
/// 延迟任务 — 指定时间后执行
/// 典型场景:订单超时取消、到期提醒、延迟通知
/// </summary>
public class OrderService
{
private readonly IBackgroundJobClient _backgroundJobs;
public OrderService(IBackgroundJobClient backgroundJobs)
{
_backgroundJobs = backgroundJobs;
}
public Guid CreateOrder(CreateOrderCommand command)
{
var orderId = Guid.NewGuid();
// 创建订单...
// 30分钟后检查是否支付,未支付则取消
_backgroundJobs.Schedule<OrderService>(
x => x.CheckOrderPaymentAsync(orderId),
TimeSpan.FromMinutes(30)
);
return orderId;
}
public async Task CheckOrderPaymentAsync(Guid orderId)
{
var order = await GetOrderAsync(orderId);
if (order.Status == OrderStatus.Pending)
{
// 未支付,取消订单并释放库存
order.Status = OrderStatus.Cancelled;
await UpdateOrderAsync(order);
Console.WriteLine($"订单 {orderId} 超时未支付,已自动取消");
}
}
}3. 定时任务(Recurring)
/// <summary>
/// 定时任务 — Cron 表达式控制执行周期
/// 典型场景:日报生成、数据同步、缓存刷新、清理过期数据
/// </summary>
public class RecurringJobScheduler
{
public static void SetupJobs()
{
// 每天凌晨2点生成日报
RecurringJob.AddOrUpdate<ReportService>(
"daily-report",
x => x.GenerateDailyReportAsync(),
"0 2 * * *" // Cron: 分 时 日 月 周
);
// 每5分钟同步数据
RecurringJob.AddOrUpdate<DataSyncService>(
"data-sync",
x => x.SyncFromErpAsync(),
"*/5 * * * *"
);
// 每小时清理过期缓存
RecurringJob.AddOrUpdate<CacheService>(
"cache-cleanup",
x => x.CleanupExpiredCacheAsync(),
"0 * * * *"
);
// 每周一早上9点发送周报
RecurringJob.AddOrUpdate<EmailService>(
"weekly-report",
x => x.SendWeeklyReportAsync(),
"0 9 * * 1"
);
// 使用 UTC 时间(推荐)
RecurringJob.AddOrUpdate<LogService>(
"log-archive",
x => x.ArchiveOldLogsAsync(),
"0 3 * * *",
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")
}
);
}
}
// 在 Program.cs 中初始化定时任务
RecurringJobScheduler.SetupJobs();常用 Cron 表达式:
| 表达式 | 说明 |
|---|---|
* * * * * | 每分钟 |
*/5 * * * * | 每5分钟 |
0 * * * * | 每小时整点 |
0 2 * * * | 每天凌晨2点 |
0 9 * * 1-5 | 工作日早上9点 |
0 0 1 * * | 每月1号零点 |
4. 延续任务(Continuation)
/// <summary>
/// 延续任务 — 前一个任务完成后执行
/// 典型场景:先处理数据,再生成报表,最后发送邮件
/// </summary>
public class ReportService
{
private readonly IBackgroundJobClient _backgroundJobs;
public ReportService(IBackgroundJobClient backgroundJobs)
{
_backgroundJobs = backgroundJobs;
}
public string GenerateMonthlyReport(int year, int month)
{
// 步骤1:收集数据
var collectJobId = _backgroundJob.Enqueue<ReportService>(
x => x.CollectDataAsync(year, month)
);
// 步骤2:收集完成后生成报表
var generateJobId = _backgroundJob.ContinueWith<ReportService>(
collectJobId,
x => x.GenerateReportFileAsync(year, month)
);
// 步骤3:生成完成后发送邮件
_backgroundJob.ContinueWith<ReportService>(
generateJobId,
x => x.SendReportEmailAsync(year, month)
);
return $"报表生成任务已提交,追踪ID:{collectJobId}";
}
public async Task CollectDataAsync(int year, int month)
{
Console.WriteLine($"正在收集 {year}年{month}月 的数据...");
await Task.Delay(3000);
}
public async Task GenerateReportFileAsync(int year, int month)
{
Console.WriteLine($"正在生成 {year}年{month}月 的报表...");
await Task.Delay(2000);
}
public async Task SendReportEmailAsync(int year, int month)
{
Console.WriteLine($"已发送 {year}年{month}月 的月报邮件");
await Task.CompletedTask;
}
}依赖注入集成
/// <summary>
/// Hangfire 任务方法支持依赖注入
/// </summary>
public class OrderCleanupJob
{
private readonly IOrderRepository _orderRepo;
private readonly ILogger<OrderCleanupJob> _logger;
private readonly IEmailService _emailService;
// 通过构造函数注入
public OrderCleanupJob(
IOrderRepository orderRepo,
ILogger<OrderCleanupJob> logger,
IEmailService emailService)
{
_orderRepo = orderRepo;
_logger = logger;
_emailService = emailService;
}
public async Task CleanupExpiredOrdersAsync()
{
_logger.LogInformation("开始清理过期订单");
var expiredOrders = await _orderRepo.GetExpiredOrdersAsync();
foreach (var order in expiredOrders)
{
order.Status = OrderStatus.Cancelled;
await _orderRepo.UpdateAsync(order);
await _emailService.SendAsync(order.UserEmail,
"订单已取消", $"您的订单 {order.OrderNo} 因超时未支付已取消");
}
_logger.LogInformation("清理完成,共处理 {Count} 个过期订单", expiredOrders.Count);
}
}
// 定时调用 — Hangfire 自动通过 DI 创建实例
RecurringJob.AddOrUpdate<OrderCleanupJob>(
"cleanup-expired-orders",
x => x.CleanupExpiredOrdersAsync(),
"*/10 * * * *"
);错误处理与重试
/// <summary>
/// 自动重试与错误处理
/// </summary>
// 默认:失败后自动重试10次,间隔递增
// 自定义重试次数
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
public async Task ProcessPaymentAsync(Guid orderId)
{
var order = await _orderRepo.GetByIdAsync(orderId);
// 支付处理...
}
// 不自动重试
[AutomaticRetry(Attempts = 0)]
public async Task SendNotificationAsync(string message)
{
// 只执行一次
}
// 全局过滤器
public class LogEverythingAttribute : JobFilterAttribute, IJobFilter
{
public void OnPerforming(PerformingContext context)
{
Console.WriteLine($"任务开始:{context.Job.Id}");
}
public void OnPerformed(PerformedContext context)
{
if (context.Exception != null)
{
Console.WriteLine($"任务失败:{context.Job.Id},异常:{context.Exception.Message}");
}
else
{
Console.WriteLine($"任务完成:{context.Job.Id}");
}
}
}Dashboard 仪表盘
访问 /hangfire 即可看到内置仪表盘:
- Jobs — 查看所有任务状态(Enqueued/Scheduled/Processing/Succeeded/Failed/Deleted)
- Recurring Jobs — 管理定时任务
- Servers — 查看服务器状态
- Dashboard — 实时统计图表
特性:
- 实时更新(WebSocket)
- 可手动触发/删除/重试任务
- 查看任务执行历史和异常信息自定义 Dashboard 授权
/// <summary>
/// 自定义 Dashboard 访问控制
/// </summary>
public class HangfireDashboardAuthFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// 仅允许管理员访问
return httpContext.User.IsInRole("Admin");
}
}
// 或者限制为仅本地访问
public class LocalOnlyAuthFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var remoteIp = httpContext.Connection.RemoteIpAddress?.ToString();
return remoteIp == "127.0.0.1" || remoteIp == "::1";
}
}实际应用案例
报表自动生成
/// <summary>
/// 报表自动生成系统
/// </summary>
public class ReportScheduler
{
public static void Setup()
{
// 每日销售报表 — 每天凌晨3点
RecurringJob.AddOrUpdate<ReportJob>(
"daily-sales-report",
x => x.GenerateDailySalesReportAsync(),
"0 3 * * *"
);
// 周报 — 每周一早上6点
RecurringJob.AddOrUpdate<ReportJob>(
"weekly-sales-report",
x => x.GenerateWeeklySalesReportAsync(),
"0 6 * * 1"
);
// 月报 — 每月1号凌晨4点
RecurringJob.AddOrUpdate<ReportJob>(
"monthly-sales-report",
x => x.GenerateMonthlySalesReportAsync(),
"0 4 1 * *"
);
}
}
public class ReportJob
{
private readonly ISalesRepository _salesRepo;
private readonly IReportGenerator _reportGenerator;
private readonly IEmailService _emailService;
private readonly ILogger<ReportJob> _logger;
public ReportJob(
ISalesRepository salesRepo,
IReportGenerator reportGenerator,
IEmailService emailService,
ILogger<ReportJob> logger)
{
_salesRepo = salesRepo;
_reportGenerator = reportGenerator;
_emailService = emailService;
_logger = logger;
}
public async Task GenerateDailySalesReportAsync()
{
_logger.LogInformation("开始生成日报");
var yesterday = DateTime.Now.AddDays(-1);
var data = await _salesRepo.GetDailySalesAsync(yesterday);
var reportPath = await _reportGenerator.GenerateExcelAsync(data, $"日报_{yesterday:yyyyMMdd}");
await _emailService.SendAsync("managers@company.com",
$"销售日报 {yesterday:yyyy-MM-dd}", "附件为昨日销售报表",
new[] { reportPath });
_logger.LogInformation("日报已发送");
}
public async Task GenerateWeeklySalesReportAsync()
{
// 类似逻辑
}
public async Task GenerateMonthlySalesReportAsync()
{
// 类似逻辑
}
}优点
缺点
总结
Hangfire 是 .NET 后台任务处理的最佳选择之一。四种任务类型覆盖了绝大多数场景,内置 Dashboard 让运维变得简单。对于需要定时任务、异步处理的后台作业系统,Hangfire 是首选方案。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《Hangfire 任务调度》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《Hangfire 任务调度》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Hangfire 任务调度》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Hangfire 任务调度》最大的收益和代价分别是什么?
