定时任务(Quartz)
本页介绍 Ape‑Volo‑Admin 中定时任务(Quartz)的使用与实现,包括核心接口、作业基类、注入与注册、Cron 表达式、并发与重试、日志与告警、以及最佳实践。
定时实现一览
- 支持基于 Quartz 的任务调度。
- 支持 Cron 表达式与固定间隔(Simple Trigger)等多种触发方式。
- 提供统一的作业执行基类,内置运行日志与失败处理。
- 通过控制器可进行任务的增删改查、启动/停止、暂停/恢复等管理。
接口功能
/// <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
/// <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
/// <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 接口。
/// <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 可获取在创建任务时注入的业务参数:
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 生成器/校验工具进行确认,并在测试环境验证触发频率。
常见操作片段
添加任务(示意):
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"
});暂停/恢复/删除:
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 中的工厂与接口实现。

