AspNet Core上实现web定时任务实例
作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用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可能没有按需正确执行。
故为在iis网站实现低频web访问下的定时任务:
设置了idle timeout =0;同时将【应用程序池】->【正在回收】->不勾选【回收条件】