视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
AspNet Core上实现web定时任务实例
2020-11-27 22:34:43 责编:小采
文档


作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用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;同时将【应用程序池】->【正在回收】->不勾选【回收条件】

    下载本文
    显示全文
    专题