Skip to content

定时任务(Quartz)

本页介绍 Ape‑Volo‑Admin 中定时任务(Quartz)的使用与实现,包括核心接口、作业基类、注入与注册、Cron 表达式、并发与重试、日志与告警、以及最佳实践。

定时实现一览

  • 支持基于 Quartz 的任务调度。
  • 支持 Cron 表达式与固定间隔(Simple Trigger)等多种触发方式。
  • 提供统一的作业执行基类,内置运行日志与失败处理。
  • 通过控制器可进行任务的增删改查、启动/停止、暂停/恢复等管理。

接口功能

csharp
/// <summary>
/// 作业调度接口
/// </summary>
public interface ISchedulerCenterService
{
    /// <summary>
    /// 开启任务
    /// </summary>
    /// <returns></returns>
    Task<bool> StartScheduleAsync();

    /// <summary>
    /// 停止任务
    /// </summary>
    /// <returns></returns>
    Task<bool> ShutdownScheduleAsync();

    /// <summary>
    /// 添加任务
    /// </summary>
    /// <param name="taskQuartz"></param>
    /// <returns></returns>
    Task<bool> AddScheduleJobAsync(QuartzNet taskQuartz);

    /// <summary>
    /// 删除任务
    /// </summary>
    /// <param name="name"></param>
    /// <param name="group"></param>
    /// <returns></returns>
    Task<bool> DeleteScheduleJobAsync(string name, string group);

    /// <summary>
    /// 暂停任务
    /// </summary>
    /// <param name="name"></param>
    /// <param name="group"></param>
    /// <returns></returns>
    Task<bool> PauseJob(string name, string group);

    /// <summary>
    /// 恢复任务
    /// </summary>
    /// <param name="name"></param>
    /// <param name="group"></param>
    /// <returns></returns>
    Task<bool> ResumeJob(string name, string group);

    /// <summary>
    /// 检测任务是否存在
    /// </summary>
    /// <param name="name"></param>
    /// <param name="group"></param>
    /// <returns></returns>
    Task<bool> IsExistScheduleJobAsync(string name, string group);


    /// <summary>
    /// 获取任务触发器状态
    /// </summary>
    /// <param name="name"></param>
    /// <param name="group"></param>
    /// <returns></returns>
    Task<TriggerState> GetTriggerStatus(string name, string group);
}

说明:

  • StartScheduleAsync/ShutdownScheduleAsync:全局启动/停止调度器。
  • Add/Delete/Pause/Resume:按任务名与组进行管理操作。
  • IsExistScheduleJobAsync:判断某个 Job 是否已存在。
  • GetTriggerStatus:获取触发器状态(如 Paused、Normal、Complete 等)。

定时任务实现基类(所有作业须继承)

位置:Ape.Volo.TaskService/JobBase.cs

csharp
/// <summary>
/// 作业调度基类(所有作业须继承)
/// </summary>
public class JobBase
{
    #region 字段

    public IQuartzNetService QuartzNetService;
    public IQuartzNetLogService QuartzNetLogService;
    public ISchedulerCenterService SchedulerCenterService;
    public ILogger<JobBase> Logger;

    #endregion

    #region 执行方法

    /// <summary>
    /// 执行指定任务
    /// </summary>
    /// <param name="context"></param>
    /// <param name="func"></param>
    protected async Task ExecuteJob(IJobExecutionContext context, Func<Task> func)
    {
        //是否成功
        bool isSuccess = true;
        //异常详情
        string exceptionDetail = string.Empty;
        //记录Job时间
        Stopwatch stopwatch = new Stopwatch();
        //taskName
        string taskName = context.JobDetail.Key.Name;
        var quartzNet = await QuartzNetService.TableWhere(x => x.TaskName == taskName).SingleAsync();
        if (quartzNet == null)
        {
            await Task.CompletedTask;
            return;
        }

        //JOB组名
        string groupName = context.JobDetail.Key.Group;
        //日志
        string jobHistory =
            $"【{DateTime.Now:yyyy-MM-dd HH:mm:ss}】【执行开始】【组别:{groupName} => 任务:{quartzNet.TaskName}";
        //耗时
        try
        {
            stopwatch.Start();
            await func(); //执行任务
            stopwatch.Stop();
            jobHistory += $",【{DateTime.Now:yyyy-MM-dd HH:mm:ss}】【执行成功】";
        }
        catch (Exception ex)
        {
            isSuccess = false;
            exceptionDetail = $" {ex.Message}\n{ex.StackTrace}";
            JobExecutionException e2 = new JobExecutionException(ex);
            //失败后是否暂停
            if (quartzNet.PauseAfterFailure)
            {
                await SchedulerCenterService.PauseJob(quartzNet.TaskName, quartzNet.TaskGroup);
            }
            else
            {
                //true  是立即重新执行任务
                e2.RefireImmediately = true;
            }

            //告警邮箱
            if (!quartzNet.AlertEmail.IsNullOrEmpty())
            {
                //实现自己的告警邮件模板 再添加队列信息
            }

            jobHistory += $",【{DateTime.Now:yyyy-MM-dd HH:mm:ss}】【执行失败:{ex.Message}】";
        }
        finally
        {
            var taskSeconds = Math.Round(stopwatch.Elapsed.TotalSeconds, 3);
            jobHistory += $",【{DateTime.Now:yyyy-MM-dd HH:mm:ss}】【执行结束】(耗时:{taskSeconds}秒)";
            if (QuartzNetService != null)
            {
                quartzNet.RunTimes += 1;
                quartzNet.UpdateTime = DateTime.Now;
                quartzNet.UpdateBy = "QuartzNet Task";

                //记录任务日志
                var quartzNetLog = new QuartzNetLog
                {
                    Id = IdHelper.NextId(),
                    TaskId = quartzNet.Id,
                    TaskName = quartzNet.TaskName,
                    TaskGroup = quartzNet.TaskGroup,
                    AssemblyName = quartzNet.AssemblyName,
                    ClassName = quartzNet.ClassName,
                    Cron = quartzNet.Cron,
                    ExceptionDetail = exceptionDetail,
                    ExecutionDuration = stopwatch.ElapsedMilliseconds,
                    RunParams = quartzNet.RunParams,
                    IsSuccess = isSuccess,
                    CreateBy = "QuartzNet",
                    CreateTime = quartzNet.UpdateTime ?? DateTime.MinValue
                };
                await QuartzNetService.UpdateJobInfoAsync(quartzNet);
                await QuartzNetLogService.CreateAsync(quartzNetLog);
            }
        }

        Logger.LogInformation(jobHistory);
    }

    #endregion
}

要点:

  • JobBase 封装了统一的执行模板:统计耗时、记录日志、异常时按配置暂停或重试、写入任务日志表。
  • 任务实体(QuartzNet)常用字段:TaskName、TaskGroup、AssemblyName、ClassName、Cron、RunParams、PauseAfterFailure、AlertEmail、RunTimes 等。
  • 失败处理:当 PauseAfterFailure = true 时失败后自动暂停;否则设置 RefireImmediately = true 进行立即重试(由 Quartz 控制)。

定时任务注入

位置:Ape.Volo.Infrastructure/Extensions/QuartzNetJobExtensions.cs

csharp
/// <summary>
/// QuartzNet作业启动器
/// </summary>
public static class QuartzNetJobExtensions
{
    public static void AddQuartzNetJobService(this IServiceCollection services)
    {
        if (services == null) throw new ArgumentNullException(nameof(services));


        services.AddSingleton<IJobFactory, JobFactory>();
        services.AddSingleton<ISchedulerCenterService, SchedulerCenterService>();
        //任务注入
        var baseType = typeof(IJob);
        var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
        var referencedAssemblies =
            Directory.GetFiles(path, GlobalType.TaskServiceAssembly + ".dll").Select(Assembly.LoadFrom).ToArray();
        var types = referencedAssemblies
            .SelectMany(a => a.DefinedTypes)
            .Select(type => type.AsType())
            .Where(x => x != baseType && baseType.IsAssignableFrom(x)).ToArray();
        var implementTypes = types.Where(x => x.IsClass).ToArray();
        foreach (var implementType in implementTypes)
        {
            services.AddTransient(implementType);
        }
    }
}

如何启用:在服务注册阶段调用 services.AddQuartzNetJobService() 即可注册 JobFactory、调度中心与自动扫描的 Job 类型。

扫描规则:会从 GlobalType.TaskServiceAssembly + ".dll" 对应程序集内查找实现了 IJob 的类型并注入容器。

使用实列

位置:Ape.Volo.TaskService/TestConsoleWriteJobService.cs 这个一个简单的定时任务示例(定时在控制台输出当前时间),继承自 JobBase 并实现 IJob 接口。

csharp
/// <summary>
/// 输出当前时间到控制台
/// </summary>
public class TestConsoleWriteJobService : JobBase, IJob
{
    public TestConsoleWriteJobService(ISchedulerCenterService schedulerCenterService,
        IQuartzNetService quartzNetService, IQuartzNetLogService quartzNetLogService,
        ILogger<TestConsoleWriteJobService> logger)
    {
        QuartzNetService = quartzNetService;
        QuartzNetLogService = quartzNetLogService;
        SchedulerCenterService = schedulerCenterService;
        Logger = logger;
    }

    public async Task Execute(IJobExecutionContext context)
    {
        await ExecuteJob(context, async () => await Run(context));
    }

    private async Task Run(IJobExecutionContext context)
    {
        await Console.Out.WriteLineAsync("当前时间:" + DateTime.Now + "\n");
        //获取传递参数
        JobDataMap data = context.JobDetail.JobDataMap;
    }
}

提示:如需避免同一 Job 并发执行(同一时刻重入),可在 Job 类上添加特性 [DisallowConcurrentExecution](Quartz 特性)。

读取 JobDataMap 参数

通过 context.JobDetail.JobDataMap 可获取在创建任务时注入的业务参数:

csharp
var data = context.JobDetail.JobDataMap;
var userId = data.GetString("userId");
var threshold = data.GetInt("threshold");

Cron 表达式速览

Quartz Cron 一般为 6~7 段:秒 分 时 日 月 周 [年]。

常用示例:

  • 每分钟执行一次:0 0/1 * * * ?
  • 每天 2:30 执行:0 30 2 * * ?
  • 工作日每晚 23:00 执行:0 0 23 ? * MON-FRI
  • 每 10 秒执行一次:0/10 * * * * ?

建议使用在线 Cron 生成器/校验工具进行确认,并在测试环境验证触发频率。


常见操作片段

添加任务(示意):

csharp
var ok = await _schedulerCenter.AddScheduleJobAsync(new QuartzNet
{
    TaskName = "SyncUserJob",
    TaskGroup = "User",
    AssemblyName = "Ape.Volo.TaskService",
    ClassName = "Ape.Volo.TaskService.SyncUserJob",
    Cron = "0 0/5 * * * ?", // 每5分钟一次
    RunParams = "{\"threshold\":100}",
    PauseAfterFailure = false,
    AlertEmail = "ops@example.com"
});

暂停/恢复/删除:

csharp
await _schedulerCenter.PauseJob("SyncUserJob", "User");
await _schedulerCenter.ResumeJob("SyncUserJob", "User");
await _schedulerCenter.DeleteScheduleJobAsync("SyncUserJob", "User");

也可通过 Ape.Volo.Api/Controllers/QuartzNetController.cs 暴露的接口进行管理。


并发、幂等与重试

  • 并发控制:为防止任务重入,建议在 Job 类上使用 [DisallowConcurrentExecution],或在业务层添加互斥(如基于 Redis 的分布式锁)。
  • 幂等设计:任务可能因失败重试或集群切换而重复触发,写操作务必具备幂等(唯一键约束、去重表、状态机校验)。
  • 失败处理:PauseAfterFailure 可直接暂停;否则默认立即重试一次。也可在实现层自定义重试策略与退避(指数退避)。
  • 事务配合:涉及多表写入时,建议使用 [UseTran]IUnitOfWork 控制事务边界,参考《事务》。

日志、告警与观测

  • JobBase 已写入 QuartzNetLog 表(含是否成功、耗时、异常详情等);可在管理界面或数据库查看。
  • 告警邮箱:配置 AlertEmail 后可在失败时触发自定义的邮件告警逻辑(需在相应位置完善模板与发送实现)。
  • 建议结合应用日志(如 Serilog)设置独立的 Job 日志 Sink,便于检索分析。

集群与部署建议(可选)

  • 单实例:默认即可运行,避免了并发与竞态的复杂性。
  • 多实例/集群:如需高可用或水平扩展,建议启用 Quartz 的持久化存储与集群特性(需配置统一的数据存储与调度器实例名/实例 ID)。
  • 分布式锁:对全局唯一执行的任务,可在业务层用 Redis 分布式锁兜底,避免跨实例重复执行。

是否启用持久化与集群取决于部署架构;如需支持,请在调度中心实现中扩展相关配置与初始化逻辑。


最佳实践与注意事项

  • 缩小执行单元:任务内只做必要工作,复杂流程拆分子任务并编排;避免长时间占用线程。
  • 避免阻塞:IO 使用异步 API;避免线程睡眠与长时间锁。
  • 外部依赖超时:为 HTTP/DB/消息操作设置超时与重试,防止堆积与雪崩。
  • 参数校验:对 RunParams 做严格校验与默认值处理,防止异常参数导致失败循环。
  • 与缓存协同:更新数据后及时失效/刷新关键缓存,参见《缓存》。
  • 可观测性:为关键步骤添加结构化日志字段(job、group、traceId、资源耗时)。

其他

定时任务的添加与管理,可以参考 Ape.Volo.Api/Controllers/QuartzNetController.cs 中的实现。 实现原理,可以参考 Ape.Volo.TaskService/Service/***.cs 中的工厂与接口实现。

版权所有 © 2021-2026 ApeVolo-Team