欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

AspNet Core上实现web定时任务实例

程序员文章站 2022-03-21 13:57:06
作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用windows计划任务/wndows service程序/crontab程序等主机方法在主机上部...

作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用windows计划任务/wndows service程序/crontab程序等主机方法在主机上部署定时任务程序/脚本。

但是很多时候,若使用的是共享主机或者受控主机,这些主机不允许你私自安装exe程序、windows服务程序。

码甲会想到在web程序中做定时任务, 目前有两个方向:

  • ①.aspnetcore自带的hostservice, 这是一个轻量级的后台服务, 需要搭配timer完成定时任务
  • ②.老牌quartz.net组件,支持复杂灵活的scheduling、支持ado/ram job任务存储、支持集群、支持监听、支持插件。

此处我们的项目使用稍复杂的quartz.net实现web定时任务。

项目背景

最近需要做一个计数程序:采用redis计数,设定每小时将当日累积数据持久化到关系型数据库sqlite。

添加quartz.net nuget 依赖包:<packagereference include="quartz" version="3.0.6" />

  • ①.定义定时任务内容: job
  • ②.设置触发条件: trigger
  • ③.将quartz.net集成进aspnet core

头脑风暴

ischeduler类包装了上述背景需要完成的第①②点工作 ,simplejobfactory定义了生成指定的job任务的过程,这个行为是利用反射机制调用无参构造函数构造出的job实例。下面是源码:

//----------------选自quartz.simpl.simplejobfactory类-------------
using system;
using quartz.logging;
using quartz.spi;
using quartz.util;
namespace quartz.simpl
{
 /// <summary> 
 /// the default jobfactory used by quartz - simply calls 
 /// <see cref="objectutils.instantiatetype{t}" /> on the job class.
 /// </summary>
 /// <seealso cref="ijobfactory" />
 /// <seealso cref="propertysettingjobfactory" />
 /// <author>james house</author>
 /// <author>marko lahma (.net)</author>
 public class simplejobfactory : ijobfactory
 {
  private static readonly ilog log = logprovider.getlogger(typeof (simplejobfactory));

  /// <summary>
  /// called by the scheduler at the time of the trigger firing, in order to
  /// produce a <see cref="ijob" /> instance on which to call execute.
  /// </summary>
  /// <remarks>
  /// it should be extremely rare for this method to throw an exception -
  /// basically only the case where there is no way at all to instantiate
  /// and prepare the job for execution. when the exception is thrown, the
  /// scheduler will move all triggers associated with the job into the
  /// <see cref="triggerstate.error" /> state, which will require human
  /// intervention (e.g. an application restart after fixing whatever
  /// configuration problem led to the issue with instantiating the job).
  /// </remarks>
  /// <param name="bundle">the triggerfiredbundle from which the <see cref="ijobdetail" />
  /// and other info relating to the trigger firing can be obtained.</param>
  /// <param name="scheduler"></param>
  /// <returns>the newly instantiated job</returns>
  /// <throws> schedulerexception if there is a problem instantiating the job. </throws>
  public virtual ijob newjob(triggerfiredbundle bundle, ischeduler scheduler)
  {
   ijobdetail jobdetail = bundle.jobdetail;
   type jobtype = jobdetail.jobtype;
   try
   {
    if (log.isdebugenabled())
    {
     log.debug($"producing instance of job '{jobdetail.key}', class={jobtype.fullname}");
    }

    return objectutils.instantiatetype<ijob>(jobtype);
   }
   catch (exception e)
   {
    schedulerexception se = new schedulerexception($"problem instantiating class '{jobdetail.jobtype.fullname}'", e);
    throw se;
   }
  }

  /// <summary>
  /// allows the job factory to destroy/cleanup the job if needed. 
  /// no-op when using simplejobfactory.
  /// </summary>
  public virtual void returnjob(ijob job)
  {
   var disposable = job as idisposable;
   disposable?.dispose();
  }
 }
}

//------------------节选自quartz.util.objectutils类-------------------------
 public static t instantiatetype<t>(type type)
{
  if (type == null)
  {
   throw new argumentnullexception(nameof(type), "cannot instantiate null");
  }
  constructorinfo ci = type.getconstructor(type.emptytypes);
  if (ci == null)
  {
   throw new argumentexception("cannot instantiate type which has no empty constructor", type.name);
  }
  return (t) ci.invoke(new object[0]);
}

很多时候,定义的job任务依赖了其他组件,这时默认的simplejobfactory不可用, 需要考虑将job任务作为依赖注入组件,加入依赖注入容器。

关键思路:

①. ischeduler 开放了jobfactory 属性,便于你控制job任务的实例化方式;

jobfactories may be of use to those wishing to have their application produce ijob instances via some special mechanism, such as to give the opportunity for dependency injection
②. aspnet core的服务架构是以依赖注入为基础的,利用aspnet core已有的依赖注入容器iserviceprovider管理job 服务的创建过程。

编码实践

① 定义job内容:

// -------每小时将redis数据持久化到sqlite, 每日凌晨跳针,持久化昨天全天数据---------------------
public class usagecountersyncjob : ijob
{
  private readonly eqiddbcontext _context;
  private readonly idatabase _redisdb1;
  private readonly ilogger _logger;
  public usagecountersyncjob(eqiddbcontext context, redisdatabase rediscache, iloggerfactory loggerfactory)
  {
   _context = context;
   _redisdb1 = rediscache[1];
   _logger = loggerfactory.createlogger<usagecountersyncjob>();
  }
   public async task execute(ijobexecutioncontext context)
  {
   // 触发时间在凌晨,则同步昨天的计数
   var _day = datetime.now.tostring("yyyymmdd");
   if (context.firetimeutc.localdatetime.hour == 0)
    _day = datetime.now.adddays(-1).tostring("yyyymmdd");

   await syncrediscounter(_day);
   _logger.loginformation("[usagecountersyncjob] schedule job executed.");
  }
  ......
 }

②注册job和trigger:

namespace eqidmanager
{
 using ioccontainer = iserviceprovider;
 // quartz.net启动后注册job和trigger
 public class quartzstartup
 {
  public ischeduler _scheduler { get; set; }

  private readonly ilogger _logger;
  private readonly ijobfactory iocjobfactory;
  public quartzstartup(ioccontainer ioccontainer, iloggerfactory loggerfactory)
  {
   _logger = loggerfactory.createlogger<quartzstartup>();
   iocjobfactory = new iocjobfactory(ioccontainer);
   var schedulerfactory = new stdschedulerfactory();
   _scheduler = schedulerfactory.getscheduler().result;
   _scheduler.jobfactory = iocjobfactory;
  }

  public void start()
  {
   _logger.loginformation("schedule job load as application start.");
   _scheduler.start().wait();

   var usagecountersyncjob = jobbuilder.create<usagecountersyncjob>()
    .withidentity("usagecountersyncjob")
    .build();

   var usagecountersyncjobtrigger = triggerbuilder.create()
    .withidentity("usagecountersynccron")
    .startnow()
    // 每隔一小时同步一次
    .withcronschedule("0 0 * * * ?")  // seconds,minutes,hours,day-of-month,month,day-of-week,year(optional field)
    .build();
   _scheduler.schedulejob(usagecountersyncjob, usagecountersyncjobtrigger).wait();

   _scheduler.triggerjob(new jobkey("usagecountersyncjob"));
  }

  public void stop()
  {
   if (_scheduler == null)
   {
    return;
   }

   if (_scheduler.shutdown(waitforjobstocomplete: true).wait(30000))
    _scheduler = null;
   else
   {
   }
   _logger.logcritical("schedule job upload as application stopped");
  }
 }

 /// <summary>
 /// iocjobfactory :实现在timer触发的时候注入生成对应的job组件
 /// </summary>
 public class iocjobfactory : ijobfactory
 {
  protected readonly ioccontainer container;

  public iocjobfactory(ioccontainer container)
  {
   container = container;
  }

  //called by the scheduler at the time of the trigger firing, in order to produce
  //  a quartz.ijob instance on which to call execute.
  public ijob newjob(triggerfiredbundle bundle, ischeduler scheduler)
  {
   return container.getservice(bundle.jobdetail.jobtype) as ijob;
  }

  // allows the job factory to destroy/cleanup the job if needed.
  public void returnjob(ijob job)
  {
  }
 }
}

③结合aspnet core 注入组件;绑定quartz.net

//-------------------------------截取自startup文件------------------------
......
services.addtransient<usagecountersyncjob>();  // 这里使用瞬时依赖注入
services.addsingleton<quartzstartup>();
......

// 绑定quartz.net
public void configure(iapplicationbuilder app, microsoft.aspnetcore.hosting.iapplicationlifetime lifetime, iloggerfactory loggerfactory)
{
  var quartz = app.applicationservices.getrequiredservice<quartzstartup>();
  lifetime.applicationstarted.register(quartz.start);
  lifetime.applicationstopped.register(quartz.stop);
}

附:iis 网站低频访问导致工作进程进入闲置状态的 解决办法

iis为网站默认设定了20min闲置超时时间:20分钟内没有处理请求、也没有收到新的请求,工作进程就进入闲置状态。

iis上低频web访问会造成工作进程关闭,此时应用程序池回收,timer等线程资源会被销毁;当工作进程重新运作,timer可能会重新生成起效, 但我们的设定的定时job可能没有按需正确执行。

AspNet Core上实现web定时任务实例

故为在iis网站实现低频web访问下的定时任务:

设置了idle timeout =0;同时将【应用程序池】->【正在回收】->不勾选【回收条件】