【.NET Core】Hangfire详解
概述
Hangfire允许您以非常简单但可靠的方式在请求管道之外启动方法调用。 这种 后台线程 中执行方法的行为称为 后台任务。
它是由:客户端、作业存储、服务端 组成的。下图描述了Hangfire的主要组织:
执行步骤
BackgroundJob.Enqueue(() => Console.WriteLine("Hello, world!"));
Enqueue
方法不会立即调用目标方法,而是运行以下步骤:
- 序列化目标方法及其所有参数。
- 根据序列化的信息创建一个新的后台任务。
- 将后台任务保存到持久化存储。
- 将后台任务入队。
执行这些步骤后, BackgroundJob.Enqueue 方法立即返回结果。轮到另一个Hangfire组件,Hangfire Server 将会从持久化存储中检查到队列中有后台任务后如期执行。
队列任务由专门的工作线程处理。每个worker将如下述流程执行任务:
- 获取一个任务,并对其他worker隐藏该任务。
- 执行任务及其所有的扩展过滤器。
- 从队列中删除该任务。
因此,只有处理成功后才能删除该任务。即使一个进程在执行期间被终止,Hangfire将执行补偿逻辑来保证每个任务都被处理。
每种持久存储各有各自的步骤和补偿逻辑机制:
- SQL Server 使用常规SQL事务,因此在进程终止的情况下,后台作业ID立即放回队列。
- MSMQ 使用事务队列,因此不需要定期检查。入队后几乎立即获取作业。
- Redis 实现使用阻塞的 BRPOPLPUSH 命令,因此与MSMQ一样立即获取作业。但是在进程终止的情况下,只有在超时到期后(默认为30分钟)才重新排队。
客户端
hangfire客户端可以创建多种类型的后台任务
var client = new BackgroundJobClient();
//创建立即执行任务
client.Enqueue(() => Console.WriteLine("Easy!"));
//创建延时执行任务
client.Delay(() => Console.WriteLine("Reliable!"), TimeSpan.FromDays(1));
Hangfire序列化任务并保存到 作业存储 后将控制权转移给某个消费者。
持久化
Hangfire将您的工作保存到持久存储中,并以可靠的方式处理它们。这意味着在您中止Hangfire工作线程,卸载应用程序域,甚至终止进程, 你的任务仍会保存起来等待处理 . Hangfire在你执行完任务的代码之前都标记状态,包括可能失败的任务状态。它包含不同的自动重试功能,存储错误的信息。
Mysql:
<PackageReference Include="Hangfire.MySqlStorage" Version="2.0.3" />
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)//此方法 只初次创建数据库使用即可
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseStorage(new MySqlStorage(connectionString, new MySqlStorageOptions
{
TransactionIsolationLevel = (System.Transactions.IsolationLevel?)System.Data.IsolationLevel.ReadCommitted, //事务隔离级别。默认是读取已提交
QueuePollInterval = TimeSpan.FromSeconds(1), //- 作业队列轮询间隔。默认值为15秒。
JobExpirationCheckInterval = TimeSpan.FromMinutes(20),
CountersAggregateInterval = TimeSpan.FromMinutes(5),
PrepareSchemaIfNecessary = true, // 如果设置为true,则创建数据库表。默认是true
DashboardJobListLimit = 50000,
TransactionTimeout = TimeSpan.FromMinutes(1),
TablesPrefix = "Hangfire",
})));
Sqlserver:
<PackageReference Include="Hangfire.SqlServer" Version="1.8.2" />
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)//此方法 只初次创建数据库使用即可
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(@"Server=.; Database=fanDB;Integrated Security=True"));
内存:
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)//此方法 只初次创建数据库使用即可
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMemoryStorage());
Redis:
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.3-beta" />
var Redis = ConnectionMultiplexer.Connect("xxxx:6379,password=123456,connectTimeout=1000,connectRetry=1,syncTimeout=10000");
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)//此方法 只初次创建数据库使用即可
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseRedisStorage(Redis,new RedisStorageOptions {
Db=2,
FetchTimeout=TimeSpan.FromSeconds(5),
InvisibilityTimeout=TimeSpan.FromSeconds(5),
Prefix="hangfire:",
SucceededListSize=1000,
DeletedListSize=1000,
//LifoQueues
//ExpiryCheckInterval=TimeSpan.FromSeconds(5),
UseTransactions=true
}));
服务端
后台任务由服务端处理。它实现一组专用(非线程池的)后台线程,用于从作业存储中取出任务并处理,服务端还负责自动删除旧数据以保持作业存储干净。
asp.net core中添加hangfire服务端:
builder.Services.AddHangfireServer(options =>
{
options.ServerName = "task";//服务名
//options.Queues = new string[] { "queue1", "queue2" };
options.SchedulePollingInterval = TimeSpan.FromSeconds(3);
options.WorkerCount = 5;//并行数
});
Hangfire Server 定期检查计划任务并将其入队,并允许worker执行。默认情况下,检查的间隔时间是 15秒, 但您可以通过SchedulePollingInterval更改它
BackgroundJobServerOptions:
- SchedulePollingInterval
SchedulePollingInterval 时间间隔越短,扫描计划作业队列的频率越高,可以更快地发现过期任务并执行,但也会增加后台处理器的负担和资源占用。
SchedulePollingInterval 时间间隔越长,则意味着扫描计划作业队列的频率越低,系统负担和资源占用也相应减少,但可能会导致过期任务无法及时执行。
需要注意的是,SchedulePollingInterval 参数仅控制计划作业队列的扫描间隔,即对那些尚未到期的计划任务不会产生影响。而一旦检测到计划任务已到期,则 Hangfire 会立即将其添加到普通作业队列中,等待后台处理器执行。因此,SchedulePollingInterval 参数的合理设置,可以帮助您控制和优化 Hangfire 后台任务处理的性能表现和资源消耗。 - WorkerCount
WorkerCount 是 Hangfire 中 BackgroundJobServerOptions 类型的一个参数,表示该服务器允许同时处理的工作者数量。简单来说,WorkerCount 参数指定了可以同时并行处理多少个作业任务,也就是可以同时运行多少个 Job。 - Queues
Queues 是 Hangfire 中 BackgroundJobServerOptions 类型的一个参数,用于指定可以处理的作业队列名称。简单来说,Queues 参数允许您将作业任务分配到不同的队列中,以便使用不同的处理器来处理这些队列,从而实现作业任务的优先级和分组管理。
//在 Enqueue 方法中,可以通过传递第二个参数 queueName 来指定要将作业添加到哪个队列中。例如,以下代码将发送电子邮件任务添加到名为 "email" 的队列
BackgroundJob.Enqueue(() => SendEmail("hello@example.com"), "email");
Demo
1、安装Nuget
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.28" />
<PackageReference Include="Hangfire.MySqlStorage" Version="2.0.3" />
添加数据库连接配置:
"ConnectionStrings": {
"DefaultConnection": "Data Source=xxxxx;User Id=root;Password=xxxxx;Database=hangfireDB;Port=3306;default command timeout=100;Connection Timeout=30;Charset=utf8mb4;Allow User Variables=true;IgnoreCommandTransaction=true"
}
2、Hangfire服务
using Hangfire;
using Hangfire.Dashboard.BasicAuthorization;
using Hangfire.MySql;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
/// <summary>
/// Hangfire扩展
/// </summary>
public static class HangfireStartupExtensions
{
/// <summary>
/// 注册Hangfire相关服务
/// </summary>
/// <param name="services"></param>
/// <param name="connectionString"></param>
public static void AddHangfire(this IServiceCollection services, string connectionString)
{
if (services == null) throw new ArgumentNullException(nameof(services));
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)//此方法 只初次创建数据库使用即可
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseStorage(new MySqlStorage(connectionString, new MySqlStorageOptions
{
TransactionIsolationLevel = (System.Transactions.IsolationLevel?)System.Data.IsolationLevel.ReadCommitted, //事务隔离级别。默认是读取已提交
QueuePollInterval = TimeSpan.FromSeconds(5), //- 作业队列轮询间隔。默认值为15秒。
JobExpirationCheckInterval = TimeSpan.FromHours(1),
CountersAggregateInterval = TimeSpan.FromMinutes(5),
PrepareSchemaIfNecessary = true, // 如果设置为true,则创建数据库表。默认是true
DashboardJobListLimit = 50000,
TransactionTimeout = TimeSpan.FromMinutes(1),
TablesPrefix = "Hangfire",
}))
.UseActivator(new JobActivator(services.BuildServiceProvider())));//依赖注入
services.AddHangfireServer();
}
添加仪表盘
<PackageReference Include="Hangfire.Dashboard.BasicAuthorization" Version="1.0.2" />
app.UseHangfireDashboard("/hangfire", DashboardOptions);
private static DashboardOptions DashboardOptions
{
get
{
var filter = new BasicAuthAuthorizationFilter(
new BasicAuthAuthorizationFilterOptions
{
SslRedirect = false,
RequireSsl = false,
LoginCaseSensitive = false,
Users = new[]
{
new BasicAuthAuthorizationUser
{
Login = "fan", //可视化的登陆账号
PasswordClear = "123456" //可视化的密码
}
}
});
return new DashboardOptions
{
Authorization = new[] { filter }
};
}
}
通过以下URL打开仪表盘:
http://
新增任务
var client = new BackgroundJobClient();
//创建立即执行任务
client.Enqueue(() => Console.WriteLine("Easy!"));
//创建延时执行任务
client.Delay(() => Console.WriteLine("Reliable!"), TimeSpan.FromDays(1));
还有更简单的方法来创建后台任务, BackgroundJob 类允许您使用静态方法创建任务。
//创建立即执行任务
BackgroundJob.Enqueue(() => Console.WriteLine("Hello!"));
//创建延时执行任务
BackgroundJob.Schedule(() => Console.WriteLine("Reliable!"), TimeSpan.FromDays(1));
周期任务:
要按照周期性(小时,每天等)调用方法,请使用 RecurringJob 类调用。您可以使用 CRON 表达式 来处理更复杂的使用场景。
RecurringJob.AddOrUpdate(() => Console.WriteLine("Daily Job"), Cron.Daily);
继续任务:
Continuations 允许您通过将多个后台任务结合起来定义复杂的工作流。
var id = BackgroundJob.Enqueue(() => Console.WriteLine("Hello, "));
BackgroundJob.ContinueWith(id, () => Console.WriteLine("world!"));
RecurringJob
类是 RecurringJobManager
类的一个入口。如果您想要更多的权力和责任,请考虑使用它
操作周期任务
//如果存在就删除周期任务
RecurringJob.RemoveIfExists("some-id");
//触发周期任务
RecurringJob.Trigger("some-id");
依赖注入
3、JobActivator
Job是通过JobActivator创建,如果job依赖其他服务,需要重写JobActivator
/// <summary>
/// Hangfire Job依赖注入
/// </summary>
public class JobActivator : Hangfire.JobActivator
{
private IServiceProvider _serviceProvider;
public JobActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object ActivateJob(Type type)
{
using var scope = _serviceProvider.CreateScope();
return scope.ServiceProvider.GetService(type);
}
}
过滤器
任务可以像ASP.NET MVC操作过滤器一样被拦截。有的时候我们希望记录Job运行生命周期内的所有事件,可以使用过滤器实现
public class LogEverythingAttribute : JobFilterAttribute,
IClientFilter, IServerFilter, IElectStateFilter, IApplyStateFilter
{
private static readonly ILog Logger = LogProvider.GetCurrentClassLogger();
public void OnCreating(CreatingContext context)
{
Logger.InfoFormat("Creating a job based on method `{0}`...", context.Job.Method.Name);
}
public void OnCreated(CreatedContext context)
{
Logger.InfoFormat(
"Job that is based on method `{0}` has been created with id `{1}`",
context.Job.Method.Name,
context.BackgroundJob?.Id);
}
public void OnPerforming(PerformingContext context)
{
Logger.InfoFormat("Starting to perform job `{0}`", context.BackgroundJob.Id);
}
public void OnPerformed(PerformedContext context)
{
Logger.InfoFormat("Job `{0}` has been performed", context.BackgroundJob.Id);
}
public void OnStateElection(ElectStateContext context)
{
var failedState = context.CandidateState as FailedState;
if (failedState != null)
{
Logger.WarnFormat(
"Job `{0}` has been failed due to an exception `{1}`",
context.BackgroundJob.Id,
failedState.Exception);
}
}
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
Logger.InfoFormat(
"Job `{0}` state was changed from `{1}` to `{2}`",
context.BackgroundJob.Id,
context.OldStateName,
context.NewState.Name);
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
Logger.InfoFormat(
"Job `{0}` state `{1}` was unapplied.",
context.BackgroundJob.Id,
context.OldStateName);
}
}
像ASP.NET过滤器一样,您可以在方法,类和全局上应用过滤器:
[LogEverything]
public class EmailService
{
[LogEverything]
public static void Send() { }
}
//全局注册
GlobalJobFilters.Filters.Add(new LogEverythingAttribute());
hangfire如何实现多负载
在不同的服务器上部署多个 Hangfire 服务,并将这些服务连接到同一个存储后端(如 SQL Server、Redis 等)。这样每个服务都会独自运行,并从存储后端中获取待执行的作业信息并执行作业。由于所有服务连接到同一个存储后端,因此它们可以共享作业的执行状态以及作业执行日志等信息。
参考:
https://m.yht7.com/news/255579
https://www.mianshigee.com/tutorial/Hangfire-zh-official/readme.md
https://www.mianshigee.com/tutorial/Hangfire-zh-official/readme.md