项目中使用Hangfire已经快一年了,期间经历过很多次的试错及升级优化,才达到现在的稳定效果。趁最近不是太忙,自己在github上做了个案列,也是拿来跟大家分享下,案例是从项目里剥离出来的,有兴趣的可以访问 这里.

什么是Hangfire

Hangfire 是一个开源的.NET任务调度框架,目前1.6+版本已支持.NET Core。个人认为它最大特点在于内置提供集成化的控制台,方便后台查看及监控:

另外,Hangfire包含三大核心组件:客户端、持久化存储、服务端,官方的流程介绍图如下:

从图中可以看出,这三个核心组件是可以分离出来单独部署的,例如可以部署多台Hangfire服务,提高处理后台任务的吞吐量。关于任务持久化存储,支持Sqlserver,MongoDb,Mysql或是Redis等等。

Hangfire基础

基于队列的任务处理(Fire-and-forget jobs)

基于队列的任务处理是Hangfire中最常用的,客户端使用BackgroundJob类的静态方法Enqueue来调用,传入指定的方法(或是匿名函数),Job Queue等参数.

var jobId = BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget!"));

在任务被持久化到数据库之后,Hangfire服务端立即从数据库获取相关任务并装载到相应的Job Queue下,在没有异常的情况下仅处理一次,若发生异常,提供重试机制,异常及重试信息都会被记录到数据库中,通过Hangfire控制面板可以查看到这些信息。

延迟任务执行(Delayed jobs)

延迟(计划)任务跟队列任务相似,客户端调用时需要指定在一定时间间隔后调用:

var jobId = BackgroundJob.Schedule(() => Console.WriteLine("Delayed!"),TimeSpan.FromDays(7));

定时任务执行(Recurring jobs)

定时(循环)任务代表可以重复性执行多次,支持CRON表达式:

RecurringJob.AddOrUpdate(() => Console.WriteLine("Recurring!"),Cron.Daily);

延续性任务执行(Continuations)

延续性任务类似于.NET中的Task,可以在第一个任务执行完之后紧接着再次执行另外的任务:

BackgroundJob.ContinueWith(jobId,() => Console.WriteLine("Continuation!"));

其实还有批量任务处理,批量任务延续性处理(Batch Continuations),但这个需要商业授权及收费。在我看来,官方提供的开源版本已经基本够用。

与quartz.net对比

在项目没有引入Hangfire之前,一直使用的是Quartz.net。个人认为Quartz.net在定时任务处理方面优势如下:

  • 支持秒级单位的定时任务处理,但是Hangfire只能支持分钟及以上的定时任务处理

原因在于Hangfire用的是开源的NCrontab组件,跟linux上的crontab指令相似。

  • 更加复杂的触发器,日历以及任务调度处理

  • 可配置的定时任务

但是为什么要换Hangfire? 很大的原因在于项目需要一个后台可监控的应用,不用每次都要从服务器拉取日志查看,在没有ELK的时候相当不方便。Hangfire控制面板不仅提供监控,也可以手动的触发执行定时任务。如果在定时任务处理方面没有很高的要求,比如一定要5s定时执行,Hangfire值得拥有。抛开这些,Hangfire优势太明显了:

  • 持久化保存任务、队列、统计信息

  • 重试机制

  • 多语言支持

  • 支持任务取消

  • 支持按指定Job Queue处理任务

  • 服务器端工作线程可控,即job执行并发数控制

  • 分布式部署,支持高可用

  • 良好的扩展性,如支持IOC、Hangfire Dashboard授权控制、Asp.net Core、持久化存储等

说了这么多的优点,我们可以有个案例,例如秒杀场景:用户下单->订单生成->扣减库存,Hangfire对于这种分布式的应用处理也是适用的,最后会给出实现。

Hangfire扩展

重点说一下上面提到的第8点,Hangfire扩展性,大家可以参考 这里,有几个扩展是很实用的.

Hangfire Dashborad日志查看

Hangfire.Console提供类似于console-like的日志体验,与Hangfire dashboard集成:

用法如下:

public void SimpleJob(PerformContext context){context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} SimpleJob Running ...");    var progressBar = context.WriteProgressBar();    foreach (var i in Enumerable.Range(1, 50).ToList().WithProgress(progressBar)){System.Threading.Thread.Sleep(1000);}
}

不仅支持日志输入到控制面板,也支持在线进度条展示.

Hangfire Dashborad授权

Hangfire.Dashboard.Authorization这个扩展应该都能理解,给Hangfire Dashboard
提供授权机制,仅授权的用户才能访问。其中提供两种授权机制:

  • OWIN-based authentication

  • Basic authentication

可以参考提供案例 ,我实现的是基本认证授权:

var options = new DashboardOptions
{AppPath = HangfireSettings.Instance.AppWebSite,AuthorizationFilters = new[]{        new BasicAuthAuthorizationFilter ( new BasicAuthAuthorizationFilterOptions{SslRedirect = false,RequireSsl = false,LoginCaseSensitive = true,Users = new[]{                new BasicAuthAuthorizationUser{Login = HangfireSettings.Instance.LoginUser,                    // Password as plain textPasswordClear = HangfireSettings.Instance.LoginPwd}}} )}
};
app.UseHangfireDashboard("", options);

IOC容器之Autofac

Hangfire对于每一个任务(Job)假如都写在一个类里,然后使用BackgroundJob/RecurringJob对方法(实例或静态)进行调用,这样会导致模块间太多耦合。实际项目中,依赖倒置原则可以降低模块之间的耦合性,Hangfire也提供了IOC扩展,其本质是重写JobActivator类。

Hangfire.Autofac是官方提供的开源扩展,用法参考如下:

GlobalConfiguration.Configuration.UseAutofacActivator(container);

RecurringJob扩展

关于RecurringJob定时任务,我写了一个扩展 RecurringJobExtensions,在使用上做了一下增强,具体有两点:

使用特性RecurringJobAttribute发现定时任务

public class RecurringJobService{[RecurringJob("*/1 * * * *")][DisplayName("InstanceTestJob")][Queue("jobs")]    public void InstanceTestJob(PerformContext context)    {context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} InstanceTestJob Running ...");}[RecurringJob("*/5 * * * *")][DisplayName("JobStaticTest")][Queue("jobs")]    public static void StaticTestJob(PerformContext context)    {context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} StaticTestJob Running ...");}
}

使用json配置文件注册定时任务

[AutomaticRetry(Attempts = 0)]
[DisableConcurrentExecution(90)]public class LongRunningJob : IRecurringJob{    public void Execute(PerformContext context)    {context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} LongRunningJob Running ...");           var runningTimes = context.GetJobData<int>("RunningTimes");context.WriteLine($"get job data parameter-> RunningTimes: {runningTimes}");               var progressBar = context.WriteProgressBar();                  foreach (var i in Enumerable.Range(1, runningTimes).ToList().WithProgress(progressBar)){Thread.Sleep(1000);}}
}

Json配置文件如下:

[{    "job-name": "Long Running Job","job-type": "Hangfire.Samples.LongRunningJob, Hangfire.Samples","cron-expression": "*/2 * * * *","job-data": {"RunningTimes": 300}}]

实现接口IRecurringJob来定义具体的定时任务,这样的写法与Quartz.net相似,可以很方便的实现Quartz.net到Hangfire的迁移。类似地,参考了quartz.net,
使用job-data-map这样的方式来定义整个任务执行期间的上下文有状态的job.

var runningTimes = context.GetJobData<int>("RunningTimes");

详细用法可以直接参考项目文档。

与MSMQ集成

Hangfire server在处理每个job时,会将job先装载到事先定义好的job queue中,比如一次性加载1000个job,在默认的sqlsever实现中是直接将这些job queue中的
job id储存到数据库中,然后再取出执行。大量的job会造成任务的延迟性执行,所以更有效的方式是将任务直接加载到MSMQ中。

实际应用中,MSMQ队列不存在时一定要手工创建,而且必须是事务性的队列,权限也要设置,用法如下:

public static IGlobalConfiguration<SqlServerStorage> 

UseMsmq(this IGlobalConfiguration<SqlServerStorage> configuration, string pathPattern, params string[] queues){    if (string.IsNullOrEmpty(pathPattern)) throw new ArgumentNullException(nameof(pathPattern));      if (queues == null)    throw new ArgumentNullException(nameof(queues));       foreach (var queueName in queues){             var path = string.Format(pathPattern, queueName);        if (!MessageQueue.Exists(path))                 using (var queue = MessageQueue.Create(path, transactional: true))queue.SetPermissions("Everyone", MessageQueueAccessRights.FullControl);}    return configuration.UseMsmqQueues(pathPattern, queues);
}

持久化存储之Redis

Hangfire中定义的job存储到sqlserver不是性能最好的选择,使用Redis存储,性能将会是巨大提升(下图来源于Hangfire.Pro.Redis).

Hangfire.Pro提供了基于servicestack.redis的redis扩展组件,然而商业收费,不开源。

但是,有另外的基于StackExchange.Redis的开源实现 Hangfire.Redis.StackExchange,
github上一直在维护,支持.NET Core,项目实测稳定可用. 该扩展相当简单:

services.AddHangfire(x =>
{    var connectionString = Configuration.GetConnectionString("hangfire.redis");x.UseRedisStorage(connectionString);
});

Hangfire最佳实践

配置最大job并发处理数

Hangfire server在启动时会初始化一个最大Job处理并发数量的阈值,系统默认为20,可以根据服务器配置设置并发处理数。最大阈值的定义除了考虑服务器配置以外,
也需要考虑数据库的最大连接数,定义太多的并发处理数量可能会在同一时间耗尽数据连接池。

app.UseHangfireServer(new BackgroundJobServerOptions
{    //wait all jobs performed when BackgroundJobServer shutdown.ShutdownTimeout = TimeSpan.FromMinutes(30),Queues = queues,WorkerCount = Math.Max(Environment.ProcessorCount, 20)
});

使用 DisplayNameAttribute特性构造缺省的JobName

public interface IOrderService : IAppService{    /// <summary>/// Creating order from product./// </summary>/// <param name="productId"></param>[AutomaticRetry(Attempts = 3)][DisplayName("Creating order from product, productId:{0}")][Queue("apis")]    void CreateOrder(int productId);
}

目前netstandard暂不支持缺省的jobname,因为需要单独引用组件System.ComponentModel.Primitives,hangfire官方给出的答复是尽量保证少的Hangfire.Core组件的依赖。

Hangfire在调用Background/RecurringJob创建job时应尽量使传入的参数简单.

Hangfire job中参数(包括参数值)及方法名都序列化为json持久化到数据库中,所以参数应尽量简单,如传入单据ID,这样才不会使Job Storage呈爆炸性增长。

为Hangfire客户端调用定义统一的REST APIs

定义统一的REST APIs可以规范并集中管理整个项目的hangfire客户端调用,同时避免到处引用hangfire组件。使用例如Swagger这样的组件来给不同的应用方(Co

/// <summary>/// Creating order from product./// </summary>/// <param name="productId"></param>/// <returns></returns>[Route("create")]
[HttpPost]public IActionResult Create([FromBody]string productId){    if (string.IsNullOrEmpty(productId))          return BadRequest();    var jobId = BackgroundJob.Enqueue<IOrderService>(x => x.CreateOrder(productId));BackgroundJob.ContinueWith<IInventoryService>(jobId, x => x.Reduce(productId));    return Ok(new { Status = 1, Message = $"Enqueued successfully, ProductId->{productId}" });
}

利用Topshelf + Owin Host将hangfire server 宿主到Windows Service.

不推荐将hangfire server 宿主到如ASP.NET appl署到windows service, 利用Topshelf+Owin Host:

/// <summary>/// OWIN host/// </summary>public class Bootstrap : ServiceControl{    private static readonly ILog _logger = LogProvider.For<Bootstrap>();    private IDisposable webApp;      public string Address { get; set; }       public bool Start(HostControl hostControl)    {           try{webApp = WebApp.Start<Startup>(Address);                   return true;}               catch (Exception ex){_logger.ErrorException("Topshelf starting occured errors.", ex);            return false;}}   

          public bool Stop(HostControl hostControl)    {        try{webApp?.Dispose();                      return true;}        catch (Exception ex){_logger.ErrorException($"Topshelf stopping occured errors.", ex);            return false;}}
}

日志配置

Hangfire 1.3.0开始,Hangfire引入了日志组件LibLog,所以应用不需要做任何改动就可以兼容如下日志组件:

  • Serilog

  • NLog

  • Log4Net

  • EntLib Logging

  • Loupe

  • Elmah

例如,配置 serilog如下,LibLog组件会自动发现并使用serilog

Log.Logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.LiterateConsole().WriteTo.RollingFile("logs\\log-{Date}.txt").CreateLogger();

Hangfire多实例部署(高可用)

下图是一个多实例Hangfire服务部署:

其中,关于Hangfire Server Node 节点可以根据实际需要水平扩展.

上述提到过一个秒杀场景:用户下单->订单生成->扣减库存,实现参考github项目Hangfire.Topshelf.

HF.Samples.Consumer

服务应用消费方(App/Webservice/Microservices等。)

HF.Samples.APIs

统一的REST APIs管理

HF.Samples.Console

Hangfire 控制面板

HF.Samples.ServerNode

Hangfire server node cli 工具,使用如下:

@echo offset dir="cluster"dotnet run -p %dir%\HF.Samples.ServerNode nodeA -q order -w 100dotnet run -p %dir%\HF.Samples.ServerNode nodeB -q storage -w 100

上述脚本为创建两个Hangfire server nodeA, nodeB分别用来处理订单、仓储服务。

-q 指定hangfire server 需要处理的队列,-w表示Hangfire server 并发处理job数量。

可以为每个job queue创建一个hangfire实例来处理更多的job.

原文地址: http://www.cnblogs.com/ecin/p/6201262.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

Hangfire项目实践分享相关推荐

  1. uniapp项目实践分享

    1.Hello uni-app:展示uni-app框架的组件.接口.模板. – by DCloud 地址:https://github.com/dcloudio/hello-uniapp/ 2.图片A ...

  2. DockOne微信分享( 九十):猎豹移动基于CoreOS在AWS上的项目实践

    本文讲的是DockOne微信分享( 九十):猎豹移动基于CoreOS在AWS上的项目实践[编者的话]本次分享介绍基于AWS的EC2服务如何设计和搭建适合自己业务的架构方案实现全球多region部署,介 ...

  3. DockOne微信分享(八十一):唯品会数据库备份恢复容器化项目实践经验总结

    本文讲的是DockOne微信分享(八十一):唯品会数据库备份恢复容器化项目实践经验总结[编者的话]本文分享了唯品会数据库Docker的异地容灾项目实践经验,项目中针对用户数据库的异地恢复场景的需求进行 ...

  4. 经验分享:如何通过项目实践提升编程能力?

    本文经授权转载自微信公众号"crossin编程教室"(crossincode) 挑选编程实践题目的过程中,有些普遍被关注的问题,今天在这里集中聊一聊,包括我这些年指导学生的一点经验 ...

  5. 案例分享 | 设计单位主导EPC项目实践经验

    面对建筑业市场的转变,越来越多的设计单位开始实施战略转型进入 EPC工程总承包领域.设计单位如何在竞争激烈的建筑市场中生存与发展, 充分发挥设计技术优势,从根源上对EPC工程总承包项目的建造成本.建设 ...

  6. 在项目实践中用更优雅的方式处理数组问题

    在最近的项目中,遇到了比较多处理数组的场景,比如要对数组里面某个元素的某一个字段进行抽取归类,或者判断数组当中的某个元素是否符满足判断条件等. 网上关于使用ES5新的的API来代替for循环的文章已经 ...

  7. 信息化基础建设系列文章汇总 .NET 平台业务项目实践

    最近一段时间一直思考和总结,如何快速,方便灵活的开发.NET平台的ERP/MIS类型软件系统. 在此之前,写过一个系列的文章来表达我的理解,一共有以下八篇文章 信息化基础建设 总体结构 信息化基础建设 ...

  8. Vue + webpack 项目实践

    最近在内部项目中做了一些基于 vue + webpack 的尝试,在小范围和同事们探讨之后,还是蛮多同学认可和喜欢的,所以通过 blog 分享给更多人. 首先,我会先简单介绍一下 vue 和 webp ...

  9. elementui 搭建布局页面路由_【项目实践】使用Vue.js和ElementUI快速实现后台管理系统的界面布局...

    前言 大家都知道,后台管理界面不需要很酷炫的动画效果,也不需要花里胡哨的界面布局,只需要简洁易用.清爽明了的界面以便于管理数据.而现在普遍的后台管理系统的界面布局都差不多,上中下结构,然后左边是导航栏 ...

最新文章

  1. 基于TF-IDF编码进行文本聚类分析:文档成对相似性计算、层次聚类(链接矩阵、树形图dendrogram绘制、聚类标签)
  2. 一个多线程应用的最佳示例
  3. 揭秘高效协作工具背后的技术架构
  4. mysql如何建立索引workbench_如何对MySQL索引进行优化分析
  5. mybatis-查询过程
  6. 第十一届蓝桥杯省赛C++组试题 第3题 求阴影面积
  7. 解析:区块链的本质是伴随信息社会产生的一种新型生产关系
  8. jenkins java版本_安装jenkins几个版本貌似都有问题
  9. 罗永浩签约抖音后,带货首秀或将献给小米,你看好这笔生意吗?
  10. cjson 使用时遇到的问题找不到库
  11. 单身暴击!程序员用 Python 给女朋友写了个翻译软件
  12. Oauth2与授权管理
  13. [AutoSar]开发工具 Vector系列和EB系列
  14. 花花省淘宝客APP源码带淘宝京东拼多多唯品会优惠券自营商城本地生活CPS外卖优惠电影票话费
  15. 原生开发什么意思_原生开发才是王道
  16. 站在知乎肩上-做更强的自己(2)
  17. uniapp仿京东地址选择插件
  18. 2018第九届蓝桥杯B组决赛题解第四题 调手表
  19. edge打开pdf不显示印章_教你PDF文档无法在edge中打开怎么解决
  20. UNIAPP手写签名

热门文章

  1. 分布式云+dubbo+zookeeper+Springmvc整合
  2. 数据结构笔记--栈的总结及java数组实现简单栈结构
  3. IE6 IE8下背景图片不显示问题
  4. 洛谷2296 寻找道路
  5. cocos2d-x 实现跨平台的目录遍历
  6. 安装 pptpd 服务
  7. 如何搭建一个指标体系
  8. HangFire循环作业中作业因执行时间太长未完成新作业开启导致重复数据的问题...
  9. 如何提升 Kestrel 上传文件的大小限制?
  10. Blazor+Dapr+K8s微服务之基于WSL安装K8s集群并部署微服务