ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Quartz.NET 任务进阶:JobDetail、JobDataMap 与作业并发状态控制完全指南

Quartz.NET 任务进阶:JobDetail、JobDataMap 与作业并发状态控制完全指南 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载本篇技术指南聚焦 Quartz.NET 3.x 教程第四课“More About Jobs”的核心内容理解IJob作业类与JobDetail定义的关系、用JobDataMap为作业传参和保存状态、借助[DisallowConcurrentExecution]与[PersistJobDataAfterExecution]控制作业的并发与状态持久化以及用JobExecutionException向调度器下达异常处理指令。读完本文你将掌握如何把一次编写、多处复用的作业类配置成多个独立调度定义并写出状态安全、并发可控的生产级 Quartz.NET 作业。本课是 Quartz.NET 3.x 官方教程的第四课建议先阅读 Lesson 1使用 Quartz 与 Lesson 3作业与触发器再回到本文深入作业内部的细节。作业类与 JobDetail一次执行一个实例你的作业类job class承载的是真正干活的代码而调度器还需要知道每个作业实例的属性——这些属性存放在JobDetail中通过流式的JobBuilder构建。回顾第一课的代码// 定义作业并把它绑定到我们的 HelloJob 类 IJobDetail job JobBuilder.CreateHelloJob() .WithIdentity(myJob, group1) .Build(); // 让触发器立即触发之后每 40 秒触发一次 ITrigger trigger TriggerBuilder.Create() .WithIdentity(myTrigger, group1) .StartNow() .WithSimpleSchedule(x x .WithIntervalInSeconds(40) .RepeatForever()) .Build(); await sched.ScheduleJob(job, trigger);作业类HelloJob长这样public class HelloJob : IJob { public async Task Execute(IJobExecutionContext context) { await Console.Out.WriteLineAsync(HelloJob is executing.); } }调度器拿到的是一份IJobDetail它记录了作业类的名称。每次执行作业时Quartz.NET 都会在调用Execute(..)之前为该类创建一个新的实例。由此得出两条硬性结论作业类必须有无参构造函数no-argument constructor作业类上的数据字段毫无意义——因为它们的值不会在上一次执行与下一次执行之间保留。[!TIP] 依赖注入 如果使用了依赖注入框架构造函数可以像 ASP.NET MVC 中的控制器一样接收服务依赖而无参构造的要求由 DI 容器代为满足。从源码看JobBuilder.cs 中的JobBuilder.CreateT()要求T : IJobBuild()时若没有显式调用.WithIdentity(...)会为你生成一个随机的Guid作为JobKey这印证了构建器始终能以一个合法状态产出 JobDetail的设计。若作业执行时彼此需要不同的输入或要在多次执行之间保留状态就要用到JobDetail上的JobDataMap。JobDataMap给作业传参、为作业保存状态JobDataMap可以存放任意数量可序列化的对象供作业实例在Execute时使用。它实现了IDictionary接口并额外提供了一组便捷方法用于存取基本类型primitive types。其定义位于 JobDataMap.cs内部基于DirtyFlagMap实现并带有脏标记机制——当作业的JobDataMap被修改时脏标记会告知调度器需要重新持久化。写入在把作业加入调度器之前放入数据// 定义作业并把它绑定到我们的 DumbJob 类 IJobDetail job JobBuilder.CreateDumbJob() .WithIdentity(myJob, group1) // 名称 myJob分组 group1 .UsingJobData(jobSays, Hello World!) .UsingJobData(myFloatValue, 3.141f) .Build();读取在作业执行期间取出数据public class DumbJob : IJob { public async Task Execute(IJobExecutionContext context) { JobKey key context.JobDetail.Key; // 注意生产代码请使用 context.MergedJobDataMap JobDataMap dataMap context.JobDetail.JobDataMap; string jobSays dataMap.GetString(jobSays); float myFloatValue dataMap.GetFloat(myFloatValue); await Console.Error.WriteLineAsync( Instance key of DumbJob says: jobSays , and val is: myFloatValue); } }类型化的读取方法GetString、GetFloat、GetInt、GetBoolean、GetDateTimeOffset以及TryGet*、泛型的GetT等定义在 DataMapExtensions.cs 中。该文件源码显示每个访问器都同时兼容以原类型存储的值与以字符串存储的值——在StoreJobDataAsStrings true的存储模式下所有数据都以不变文化invariant culture字符串落库读取时会先匹配存储类型再按CultureInfo.InvariantCulture解析字符串。因此同一份读取代码无论数据是以对象形式还是字符串形式持久化的都能正确工作。持久化 JobStore 下的序列化注意事项如果使用了持久化 JobStore参见 Job Stores放入JobDataMap的内容要仔细斟酌其中的对象会被序列化因此容易遭遇类版本兼容问题。标准 .NET 类型是安全的对于你自己定义的类任何对已有序列化实例的类的修改都必须保证兼容性不被破坏例如不删除字段、不改变字段语义。AdoJobStore与JobDataMap可以切换到一种只存原始类型与字符串的模式从根本上规避日后的序列化问题。该开关正是StoreJobDataAsStrings配置项在 AdoJobStoreBase.cs 中它被读取为usePropertiesStdAdoDelegate.cs 中的CanUseProperties即由此决定。注意源码注释强调一个已经以 blob 形式写入数据的存储不能直接切换到字符串模式而不重写数据所以该选项应在部署初期就定好。属性注入让 JobFactory 替你赋值如果你的作业类中有公有set访问器、且属性名与JobDataMap的键一致的属性Quartz.NET 的默认JobFactory会在实例化作业时自动调用这些 setter于是Execute中就不再需要手动读取 Map。自定义的JobFactory默认不会做这件事。public class DumbJob : IJob { public string JobSays { private get; set; } public float MyFloatValue { private get; set; } public async Task Execute(IJobExecutionContext context) { JobKey key context.JobDetail.Key; JobDataMap dataMap context.MergedJobDataMap; // 注意与上一例的差别 IListDateTimeOffset state (IListDateTimeOffset)dataMap[myStateData]; state.Add(DateTimeOffset.UtcNow); await Console.Error.WriteLineAsync( Instance key of DumbJob says: JobSays , and val is: MyFloatValue); } }类变得更长了但Execute()内部更干净。除了按字符串键写入JobBuilder.cs 还提供了强类型的UsingJobDataTValue(ExpressionFuncTJob, TValue jobProperty, TValue value)重载——直接以表达式指定作业属性而非拼写键名避免键名拼错与类型不符枚举属性会按枚举名存储。Trigger 也有 JobDataMap以及 MergedJobDataMap 的合并规则触发器也可以拥有自己的JobDataMap。当一个已存储的作业被多个触发器触发、且每个触发器要传入不同数据时就该把数据放在触发器的 Map 里。JobExecutionContext上的JobDataMap是JobDetail的JobDataMap与Trigger的JobDataMap合并的结果触发器中同名键的值会覆盖作业中的值。合并逻辑见 JobExecutionContextImpl.cs 的注释——它是 JobDetail 上的 Map 与 Trigger 上的 Map 的合并后者的同名值覆盖前者因此作业的Execute代码从该 Map 取数据被视为最佳实践。同时该实现也明确指出这个 Map 是本次触发firing专属的一份拷贝写入它是安全的作业、中间件、监听器都能看到但任何修改都不会回写到 JobDetail 或 Trigger 的已存储数据。读取合并后的 JobDataMappublic class DumbJob : IJob { public async Task Execute(IJobExecutionContext context) { JobKey key context.JobDetail.Key; JobDataMap dataMap context.MergedJobDataMap; // 注意与上一例的差别 string jobSays dataMap.GetString(jobSays); float myFloatValue dataMap.GetFloat(myFloatValue); IListDateTimeOffset state (IListDateTimeOffset)dataMap[myStateData]; state.Add(DateTimeOffset.UtcNow); await Console.Error.WriteLineAsync( Instance key of DumbJob says: jobSays , and val is: myFloatValue); } }作业实例一个类多个定义一个作业类可以在调度器中拥有许多实例定义创建多个JobDetail每个都有自己的属性与JobDataMap然后把它们全部加入调度器即可。例如作业类SalesReportJob从自己的JobDataMap读取销售员姓名。你可以创建两个JobDetail——SalesReportForJoyce和SalesReportForMike分别把Joyce与Mike放进各自的JobDataMap。当触发器触发时其关联的 JobDetail 会被加载作业类由调度器的JobFactory实例化默认JobFactory用Activator.CreateInstance调用作业类的默认构造函数再调用与JobDataMap键匹配的 setter 属性。编写自己的JobFactory可以实现让 IoC/DI 容器来创建并初始化作业实例Quartz.NET 3.x 中MicrosoftDependencyInjectionJobFactory正是如此可配合扩展包使用。术语约定术语含义job definition 或 JobDetail instance已存储的 JobDetailjob instance 或 instance of a job definition正在执行的作业job通常指一个具名的定义即 JobDetailjob type实现作业接口的类作业状态与并发两个关键特性作业类上的两个特性attribute会改变 Quartz.NET 对作业状态即 JobDataMap与并发的处理方式。[DisallowConcurrentExecution]告诉 Quartz.NET不要并发执行同一个作业定义的多个实例。它作用于每个 JobDetail按定义而不是按作业类如果SalesReportJob标注了该特性同一时刻只会有一个SalesReportForJoyce在执行但SalesReportForMike可以与它并行运行。特性之所以写在类上是因为它往往会影响类的编写方式。[DisallowConcurrentExecution] public class SalesReportJob : IJob { public async Task Execute(IJobExecutionContext context) { // 同一 JobDetail 定义不会并发执行 } }从 DisallowConcurrentExecutionAttribute.cs 的源码看该特性的AttributeUsage允许Class | Interface——即可以标注在作业类上也可以标注在接口上便于对一组实现同一接口的作业统一约束。JobBuilder.cs 还提供了等价的流式方法.DisallowConcurrentExecution(bool)若未显式设置调度器会在作业类型自身、其祖先类或实现的接口标注了该特性时禁止并发。[PersistJobDataAfterExecution]告诉 Quartz.NET 在Execute()完成之后更新 JobDetail 已存储的JobDataMap——即使Execute抛出了JobExecutionException也会更新。这样同一个 JobDetail 的下一次执行拿到的就是更新后的值而不是最初存储的值。它同样作用于每个作业定义并且标注在类上因为Execute中的代码必须为有状态而设计。[PersistJobDataAfterExecution] public class StatefulJob : IJob { public async Task Execute(IJobExecutionContext context) { var dataMap context.MergedJobDataMap; int count dataMap.GetInt(count) 1; dataMap[count] count; // 本次执行后写回 JobDetail 的存储 } }其定义见 PersistJobDataAfterExecutionAttribute.csAttributeUsage同样是Class | InterfaceJobBuilder.cs 也提供了.PersistJobDataAfterExecution(bool)方法。注意 JobDataMap.cs 的注释带该特性的作业实例每次执行后其JobDataMap会被重新持久化——这正是脏标记机制发挥作用的地方。两者的搭配建议如果你使用了PersistJobDataAfterExecution强烈建议同时使用[DisallowConcurrentExecution]。否则当同一个 JobDetail 的两个实例并发运行时最终存储下来的是哪一份数据将成为一场竞态race。这是因为两次并发执行都可能在互相覆盖 JobDataMap 的写入结果。JobDetail 的其他属性你还可以在 JobDetail 上设置其他属性属性效果Durability非持久non-durable作业在没有与之关联的活动触发器时会被调度器自动删除。RequestsRecovery若作业在硬关机hard shutdown期间正在执行调度器再次启动时会重新执行该作业。硬关机hard shutdown指的是进程崩溃或机器断电。当作业以这种方式被重新执行时JobExecutionContext.Recovering会返回true该属性定义于 IJobExecutionContext.cs实现中还会从合并后的 JobDataMap 恢复出原触发器的TriggerKey。在 JobBuilder.cs 中两者分别对应.StoreDurably(bool)与.RequestRecovery(bool)流式方法默认值都是false。JobExecutionException向调度器下达指令从IJob.Execute(..)中唯一可以抛出的异常就是JobExecutionException因此通常的做法是把整个方法体包进 try-catch。JobExecutionException还可以携带给调度器的处理指令——详见其文档与下面的源码说明。JobExecutionException.cs 定义了三个 init-only 指令属性用对象初始化器设置// 立即以同一个 JobExecutionContext 重新触发 throw new JobExecutionException(import failed, ex) { RefireImmediately true };指令属性含义RefireImmediately立即以同一个IJobExecutionContext重新触发该作业若该标志为 true以下两个注销指令会被忽略。UnscheduleFiringTrigger注销触发本次执行的触发器。UnscheduleAllTriggers注销该作业的所有触发器。此外该类型是sealed的有意设计为对调度器的三条指令作业自己附加的状态应通过Exception.Data携带例如throw new JobExecutionException(import failed, ex) { Data { [tenant] tenantId } }作业抛出的实例会原样交给IJobListener.JobWasExecuted。若抛出的不是JobExecutionException则会以JobExecutionException→JobExecutionProcessException→ 你的异常的两层包装形式到达监听器。小结作业类是干活的模板JobDetail才是调度器眼中的定义两者通过JobBuilder绑定每次执行都会新实例化作业类因此无参构造函数是硬性要求。JobDataMap是作业传参与状态保存的唯一官方通道JobDetail 与 Trigger 各有一份执行期读取MergedJobDataMap获得合并视图Trigger 同名值覆盖 Job写入只影响本次 firing需要跨执行保留状态则配合[PersistJobDataAfterExecution]。一个作业类可注册多个JobDetail如SalesReportForJoyce/SalesReportForMike实现同类不同数据的复用。并发与状态用[DisallowConcurrentExecution]与[PersistJobDataAfterExecution]控制二者通常应成对使用。作业的异常处理统一收敛到JobExecutionException其三个指令属性RefireImmediately、UnscheduleFiringTrigger、UnscheduleAllTriggers让作业能把接下来怎么办的决定权交回调度器。延伸阅读Lesson 5更多关于触发器——触发器的JobDataMap、优先级与 misfire 指令Lesson 10JobStores——RAMJobStore 与 AdoJobStore以及StoreJobDataAsStrings的完整上下文核心源码JobBuilder.cs、JobDataMap.cs、JobExecutionContextImpl.cs、JobExecutionException.cs赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐终极指南Quartz.NET任务参数传递与状态管理的JobDataMap完全掌握终极指南Quartz.NET任务参数传递与状态管理的JobDataMap完全掌握 Quartz.NET是一个功能强大的企业级任务调度框架而JobDataMa任务调度后端Quartz.NET 任务与触发器Jobs and Triggers核心概念详解IJob、JobDetail、JobDataMap 与 Trigger 编程模型Quartz.NET 任务与触发器Jobs and Triggers核心概念详解IJob、JobDetail、JobDataMap 与 Trigger 编任务调度后端如何用Chunky快速预生成Minecraft区块新手必备的完整指南如何用Chunky快速预生成Minecraft区块新手必备的完整指南 Chunky是一款专为Minecraft服务器设计的区块预生成工具能够帮助服务器管理员上一篇社区贡献指南如何参与tiny-random-mistral-openmind项目的开发与改进下一篇ExplorerPatcher深度解析Windows 11 23H2累积更新兼容性危机与解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表