【.NET Core】Hangfire详解

概述

Hangfire允许您以非常简单但可靠的方式在请求管道之外启动方法调用。 这种 后台线程 中执行方法的行为称为 后台任务。
它是由:客户端、作业存储、服务端 组成的。下图描述了Hangfire的主要组织:

执行步骤

BackgroundJob.Enqueue(() => Console.WriteLine("Hello, world!"));

Enqueue 方法不会立即调用目标方法,而是运行以下步骤:

  1. 序列化目标方法及其所有参数。
  2. 根据序列化的信息创建一个新的后台任务。
  3. 将后台任务保存到持久化存储。
  4. 将后台任务入队。

执行这些步骤后, BackgroundJob.Enqueue 方法立即返回结果。轮到另一个Hangfire组件,Hangfire Server 将会从持久化存储中检查到队列中有后台任务后如期执行。
队列任务由专门的工作线程处理。每个worker将如下述流程执行任务:

  1. 获取一个任务,并对其他worker隐藏该任务。
  2. 执行任务及其所有的扩展过滤器。
  3. 从队列中删除该任务。

因此,只有处理成功后才能删除该任务。即使一个进程在执行期间被终止,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:///hangfire

新增任务

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

posted @ 2022-05-23 21:59  .Neterr  阅读(4154)  评论(2编辑  收藏  举报