皮皮网

皮皮网

【财务目标指标源码】【传人记源码】【plm web源码】scheduler源码

时间:2024-11-19 02:45:04 分类:热点

1.深入理解 RxJava2:Scheduler(2)
2.react源码解析(二)时间管理大师fiber
3.PyTorch 源码解读之 torch.optim:优化算法接口详解
4.《深入理解react》之调度引擎——Scheduler
5.可动态配置的Schedule设计
6.JobScheduler的使用和原理

scheduler源码

深入理解 RxJava2:Scheduler(2)

       欢迎来到深入理解 RxJava2 系列第二篇,本文基于 RxJava 2.2.0 正式版源码,将探讨 Scheduler 与 Worker 的概念及其实现原理。

       Scheduler 与 Worker 在 RxJava2 中扮演着至关重要的角色,它们是线程调度的核心与基石。虽然 Scheduler 的财务目标指标源码作用较为熟悉,但 Worker 的概念了解的人可能较少。为何在已有 Scheduler 的情况下,还要引入 Worker 的概念呢?让我们继续探讨。

       首先,Scheduler 的核心定义是调度 Runnable,支持立即、延时和周期性调用。而 Worker 是任务的最小单元的载体。在 RxJava2 内部实现中,通常一个或多个 Worker 对应一个 ScheduledThreadPoolExecutor 对象,这里暂不深入探讨。

       在 RxJava 1.x 中,Scheduler 没有 scheduleDirect/schedulePeriodicallyDirect 方法,只能先创建 Worker,再通过 Worker 来调度任务。这些方法是对 Worker 调度的简化,可以理解为创建一个只能调度一次任务的 Worker 并立即调度该任务。在 Scheduler 基类的源码中,默认实现是直接创建 Worker 并创建对应的 Task(虽然在部分 Scheduler 的覆盖实现上并没有创建 Worker,但可以认为存在虚拟的 Worker)。

       一个 Scheduler 可以创建多个 Worker,这两者是一对多的关系,而 Worker 与 Task 也是一对多的关系。Worker 的存在旨在确保两件事:统一调度 Runnable 和统一取消任务。例如,在 observeOn 操作符中,可以通过 Worker 来统一调度和取消一系列的 Runnable。

       RxJava2 默认内置了多种 Scheduler 实现,适用于不同场景,这些 Scheduler 都可以在 Schedulers 类中直接获得。以下是两个常用 Scheduler 的源码分析:computation 和 io。

       NewThreadWorker 在 computation、io 和 newThread 中都有涉及,下面简单了解一下这个类。NewThreadWorker 与 ScheduledThreadPoolExecutor 之间是一对一的关系,在构造函数中通过工厂方法创建一个 corePoolSize 为 1 的 ScheduledThreadPoolExecutor 对象并持有。

       ScheduledThreadPoolExecutor 从 JDK1.5 开始存在,这个类继承于 ThreadPoolExecutor,支持立即、延时和周期性任务。但是注意,在 ScheduledThreadPoolExecutor 中,maximumPoolSize 参数是无效的,corePoolSize 表示最大线程数,且它的队列是无界的。这里不再深入探讨该类,否则会涉及太多内容。传人记源码

       有了这个类,RxJava2 在实现 Worker 时就站在了巨人的肩膀上,线程调度可以直接使用该类解决,唯一的麻烦之处就是封装一层 Disposable 的逻辑。

       ComputationScheduler 是计算密集型的 Scheduler,其线程数与 CPU 核心数密切相关。当线程数远超过 CPU 核心数目时,CPU 的时间更多地损耗在了线程的上下文切换。因此,保持最大线程数与 CPU 核心数一致是比较通用的方式。

       FixedSchedulerPool 可以看作是固定数量的真正 Worker 的缓存池。确定了 MAX_THREADS 后,在 ComputationScheduler 的构造函数中会创建 FixedSchedulerPool 对象,FixedSchedulerPool 内部会直接创建一个长度为 MAX_THREADS 的 PoolWorker 数组。PoolWorker 继承自 NewThreadWorker,但没有任何额外的代码。

       PoolWorker 的使用方法是从池子里取一个 PoolWorker 并返回。但是需要注意,每个 Worker 是独立的,每个 Worker 内部的任务是绑定在这个 Worker 中的。如果按照上述方法暴露 PoolWorker,会出现两个问题:

       为了解决上述问题,需要在 PoolWorker 外再包一层 EventLoopWorker。EventLoopWorker 是一个代理对象,它会将 Runnable 代理给 FixedSchedulerPool 中取到的 PoolWorker 来调度,并负责管理通过它创建的任务。当自身被取消时,会将创建的任务全部取消。

       与 ComputationScheduler 恰恰相反,IoScheduler 的线程数是无上限的。这是因为 IO 设备的速度远低于 CPU 速度,在等待 IO 操作时,CPU 往往是闲置的。因此,应该创建更多的线程让 CPU 尽可能地利用。当然,并不是线程越多越好,线程数目膨胀到一定程度会影响 CPU 的效率,也会消耗大量的内存。在 IoScheduler 中,每个 Worker 在空置一段时间后就会被清除以控制线程的数目。

       CachedWorkerPool 是一个变长并定期清理的 ThreadWorker 的缓存池,内部通过一个 ConcurrentLinkedQueue 维护。和 PoolWorker 类似,ThreadWorker 也是继承自 NewThreadWorker。仅仅是增加了一个 expirationTime 字段,用来标识这个 ThreadWorker 的超时时间。

       在 CachedWorkerPool 初始化时,会传入 Worker 的超时时间,目前是写死的 秒。这个超时时间表示 ThreadWorker 闲置后最大存活时间(实际中不保证 秒时被回收)。

       IoScheduler 中也存在一个 EventLoopWorker 类,plm web源码它和 ComputationScheduler 中的作用类似。因为 CachedWorkerPool 是每隔 秒清理一次队列的,所以 ThreadWorker 的存活时间取决于入队的时机。如果一直没有被再次取出,其被实际清理的延迟在 - 秒之间。

       熟悉线程的读者会发现,ComputationScheduler 与 IoScheduler 很像某些参数下的 ThreadPoolExecutor。它们对线程的控制外在表现很相似,但实际的线程执行对象不一样。这两者的对比有助于我们更深刻地理解 Scheduler 设计的内在逻辑。

       Scheduler 是 RxJava 线程的核心概念,RxJava 基于此屏蔽了 Thread 相关的概念,只与 Scheduler/Worker/Runnable 打交道。

       本来计划继续基于 Scheduler 和大家一起探讨 subscribeOn 与 observeOn,但考虑到篇幅问题,这些留待下篇分享。

       感谢大家的阅读,欢迎关注笔者的公众号,可以第一时间获取更新,同时欢迎留言沟通。

react源码解析(二)时间管理大师fiber

       React的渲染和对比流程在面对大规模节点时,会消耗大量资源,影响用户体验。为了改进这一情况,React引入了Fiber机制,成为时间管理大师,平衡了浏览器任务和用户交互的响应速度。

       Fiber的中文翻译为纤程,是一种内部更新机制,支持不同优先级的任务管理,具备中断与恢复功能。每个任务对应于React Element的Fiber节点。Fiber允许在每一帧绘制时间(约.7ms)内,合理分配计算资源,优化性能。

       相比于React,React引入了Scheduler调度器。当浏览器空闲时,Scheduler会决定是否执行任务。Fiber数据结构具备时间分片和暂停特性,更新流程从递归转变为可中断的循环,通过shouldYield判断剩余时间,灵活调整更新节奏。

       Scheduler的关键实现是requestIdleCallback API,它用于高效地处理碎片化时间,提高用户体验。尽管部分浏览器已支持该API,React仍提供了requestIdleCallback polyfill,以确保跨浏览器兼容性。

       在Fiber结构中,每个节点包含返回指针(而非直接的父级指针),这个设计使得子节点完成工作后能返回给父级节点。sip注册源码这种机制促进了任务的高效执行。

       Fiber的遍历遵循深度优先原则,类似王朝继承制度,确保每一帧内合理分配资源。通过实现深度优先遍历算法,可以构建Fiber树结构,用于渲染和更新DOM元素。

       为了深入了解Fiber,可以使用本地环境调试源码。通过创建React项目并配置调试环境,可以观察Fiber节点的结构和行为。了解Fiber的遍历流程和结构后,可以继续实现一个简单的Fiber实例,这有助于理解React渲染机制的核心。

       Fiber架构是React的核心,通过时间管理机制优化了性能,使React能够在大规模渲染时保持流畅。了解Fiber的交互流程和遍历机制,有助于深入理解React渲染流程。未来,将详细分析优先级机制、断点续传和任务收集等关键功能,揭示React是如何高效地对比和更新DOM树的。

       更多深入学习资源和讨论可参考以下链接:

       《React技术揭秘》

       《完全理解React Fiber》

       《浅谈 React Fiber》

       《React Fiber 源码解析》

       《走进 React Fiber 的世界》

PyTorch 源码解读之 torch.optim:优化算法接口详解

       本文深入解读了 PyTorch 中的优化算法接口 torch.optim,主要包括优化器 Optimizer、学习率调整策略 LRScheduler 及 SWA 相关优化策略。以下为详细内容:

       Optimizer 是所有优化器的基类,提供了初始化、更新参数、设置初始学习率等基本方法。在初始化优化器时,需要传入模型的可学习参数和超参数。Optimizer 的核心方法包括:

       1. 初始化函数:创建优化器时,需指定模型的可学习参数和超参数,如学习率、动量等。

       2. add_param_group:允许为模型的不同可学习参数组设置不同的超参数,以适应不同的学习需求。

       3. step:执行一次模型参数更新,需要闭包提供损失函数的梯度信息。

       4. zero_grad:在更新参数前,清空参数的梯度信息。

       5. state_dict 和 load_state_dict:用于序列化和反序列化优化器的状态,便于保存和加载模型的训练状态。

       Optimizer 包括常见的优化器如 SGD、Adagrad、RMSprop 和 Adam,各有特点,适用于不同的应用场景。例如,SGD 适用于简单场景,而 Adam 则在处理大数据集时表现更优。建站公司 源码

       学习率调节器 lr_scheduler 则负责在训练过程中调整学习率,以适应模型的收敛过程。PyTorch 提供了多种学习率调整策略,如 StepLR、MultiStepLR、ExponentialLR 等,每种策略都有其特点和应用场景,如 StepLR 用于周期性调整学习率,以加速收敛。

       SWA(随机权重平均)是一种优化算法,通过在训练过程中计算模型参数的平均值,可以得到更稳定的模型,提高泛化性能。SWA 涉及 AveragedModel 类,用于更新模型的平均参数,以及 update_bn 函数,用于在训练过程中更新批量归一化参数。

       总结,torch.optim 提供了丰富的优化算法接口,可以根据模型训练的需求灵活选择和配置,以达到最佳的训练效果和泛化性能。通过深入理解这些优化器和学习率调整策略,开发者可以更有效地训练深度学习模型。

《深入理解react》之调度引擎——Scheduler

       深入理解react

       在react 版本发布以来的近两年时间里,许多伙伴都体验到了并发模式带来的爽感,createRoot()的使用让应用有了更流畅的体验。而这一切的核心,便是react执行流中的调度引擎——Scheduler。调度,这个概念在计算机行业中广泛存在,无论是操作系统、浏览器还是大型应用,都离不开调度任务的需求。Scheduler,作为独立的包,不仅可以在react中使用,更可以在任何其他库中发挥作用,其简洁的源码使深入理解react成为可能。

       为何需要调度器?首先是为了解决卡顿问题。在js引擎和渲染绘制都在同一线程执行的情况下,如何保证帧的刷新频率不被CPU密集型任务阻塞?其次,react会生成具有优先级的任务,优先级高的任务可能在后面产生,调度器能确保优先级高的任务优先执行,以提升用户体验。

       Scheduler通过暴露的方法如unstable_scheduleCallback,可以按照优先级的高低顺序调度任务,并保证异步执行。在实际体验中,我们可以创建工程来测试Scheduler的执行时机,发现它会遵循优先级顺序,优先执行高优先级任务,并在下一个宏任务中异步执行。

       源码解析中,小根堆作为关键数据结构,用于维护优先级队列。Scheduler使用小根堆来管理任务,优先级最高的任务始终处于堆顶。优先级的动态调整确保了任务在调度过程中的灵活排序。例如,随着时间推移,新任务的优先级会逐渐提高,使得原有任务在下一个周期中优先执行。

       Scheduler的核心逻辑在工作循环中体现,通过合理调度不同优先级的任务,既不阻碍UI绘制,又能高效执行任务。对于大任务,用户可以通过拆分策略,将其划分为多个小任务,以避免阻塞UI,实现流畅的用户体验。

       最后,Scheduler在react中扮演着关键角色,通过合理的任务调度,确保应用流畅运行。深入理解Scheduler,将为深入理解react提供坚实的基础。关注专栏,获取更多react相关知识。

可动态配置的Schedule设计

       1.背景

       定时任务是实际开发中常见的一类功能,例如每天早上凌晨对前一天的注册用户数量、渠道来源进行统计,并以邮件报表的方式发送给相关人员。相信这样的需求,每个开发伙伴都处理过。

       你可以使用Linux的Crontab启动应用程序进行处理,或者直接使用Spring的Schedule对任务进行调度,还可以使用分布式调度系统,如果xxl-job等。相信你已经轻车熟路、习以为常。直到有一天你接到了一个新需求:

       1.新建一组任务,周期性的执行指定SQL并将结果以邮件的方式发送给特定人群;2.比较方便的对任务进行管理,比如启动、停止,修改调度周期等;3.动态添加、移除任务,不需要频繁的修改、发布程序;

       停顿几分钟,简单思考一下,有哪几种实现思路呢?

       本篇文章将从以下几部分进行讨论:

       1.SpringSchedule配置和使用。首先我们将介绍Demo的骨架,并基于Spring-Boot完成Schedule的配置;2.数据库定时轮询方案。使用SpringSchedule定时轮询数据库,并执行相应任务。在执行任务策略中,我们将尝试同步和异步执行两种方案,并对其优缺点进行分析;3.基于TaskScheduler动态配置方案。基于数据库轮询或配置中心两种方案动态的对SpringTaskScheduler进行配置,以实现动态管理任务的目的;4.我们进入分布式环境,利用多个冗余节点解决系统高可用问题,同时使用分布式锁保障只会有一个任务同时执行;

2.SpringSchedule

       SpringBoot上的Schedule的使用非常简单,无需增加新的依赖,只需简单配置即可。

       1.使用@EnableScheduling启用Schedule;2.在要调度的方法上增加@Scheduled;

       首先,我们需要在启动类上添加@EnableScheduling注解,该注解将启用SchedulingConfiguration配置类帮我们完成最基本的配置。

@SpringBootApplication@EnableSchedulingpublicclassConfigurableScheduleDemoApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(ConfigurableScheduleDemoApplication.class,args);}}

       启用Schedule配置之后,在需要被调度的方法上增加@Scheduled注解。

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}

       runTask任务延迟1s进行初始化,并以5s为间隔进行调度。

       Scheduled注解类的详细配置如下:

配置含义样例cronlinuxcrontab表达式@Scheduled(cron="*/5****MON-FRI")工作日,每5s调度一次fixedDelay固定间隔,上次运行结束,与下次启动运行,相隔固定时长@Scheduled(fixedDelay=)运行结束后,5S后启动一次调度fixedDelayString与fixedDelay一致fixedRate固定周期,前后两次运行相隔固定的时长@Scheduled(fixedRate=)前后两个任务,间隔5秒fixedRateString与fixedRate一致initialDelay第一次执行,间隔时间@Scheduled(initialDelay=,fixedRate=)第一次执行,延时1秒,以后以5秒为周期进行调度initialDelayString与initialDelay一致

       环境搭建完成,让我们开始第一个方案。

3.数据库定时轮询

       使用数据库来管理任务,通过轮询的方案,进行动态调度。首先,我们看下最简单的方案:串行执行方案。

3.1.串行执行方案

       整体思路非常简单,流程如下:

       主要分如下几步:

       1.在应用中启动一个Schedule任务(每1秒调度一次),定时从数据库中获取待执行的任务(状态为可用,下一次执行时间小于当前时间);2.根据数据库的任务配置信息,依次遍历并执行任务;3.任务执行完成后,经过计算获得下一次调度时间,将其写回到数据库;4.等待下一次任务调度。

       核心代码如下:

@Scheduled(fixedDelay=,initialDelay=)publicvoidloadAndRunTask(){ Datenow=newDate();//加载需要运行的任务://1.状态为ENABLE//2.下一次运行时间小于当前时间List<TaskDefinitionV2>shouldRunTasks=loadShouldRunTasks(now);//依次遍历待运行任务,执行对于的任务for(TaskDefinitionV2task:shouldRunTasks){ //DoubleCheckif(task.shouldRun(now)){ //执行任务runTask(task);//更新任务的下一次运行时间updateNextRunTime(task,now);}}}

       方案简单但非常有效,那该方案存在哪些问题呢?最主要的问题就是:任务串行执行,会导致后面任务出现延时运行;同时,下一轮检查也会被delay。

       例如,依次加载了待执行任务task1、task2、task3。其中task1耗时5秒,task2耗时5秒,task3耗时1秒,由于三个任务串行执行,task2将延时5秒,task3延时秒;下一轮检查距上次启动相差秒。

       究其根本,核心问题是调度线程和运行线程是同一个线程,调度的运行和任务的运行相互影响。

       让我们看一个改进方案:并行执行方案。

3.2.并行执行方案

       整体执行流程如下:

       相比之前的方案,新方案引入了线程池,每一个任务对应一个线程池,避免任务间的相互影响;任务在线程池中异步处理,避免了调度线程的延时。具体流程如下:

       1.步骤一不变,在应用中启动一个Schedule任务(每1秒调度一次),定时从数据库中获取待执行的任务(状态为可用,下一次执行时间小于当前时间);2.依次遍历任务,将任务提交到专有线程池中异步执行,调度线程直接返回;3.任务在线程池中运行,结束后更新下一次的运行时间;4.调度线程重新从数据库中获取待执行任务,在将任务提交至线程池中,如果有任务正在执行,使用线程池拒绝策略,抛弃最老的任务;

       核心代码如下:

       Spring调度任务,每1秒运行一次:

@Scheduled(fixedDelay=,initialDelay=)publicvoidloadAndRunTask(){ Datenow=newDate();//加载所有待运行的任务//1.状态为ENABLE//2.下一次运行时间小于当前时间List<TaskDefinitionV2>shouldRunTasks=loadShouldRunTasks(now);//遍历待运行任务for(TaskDefinitionV2task:shouldRunTasks){ //1.根据TaskId获取任务对应的线程池//2.将任务提交至线程池中this.executorServiceForTask(task.getId()).submit(newTaskRunner(task.getId()));}}

       自定义线程池,每个线程池最多只有一个线程,空闲超过秒后,线程自动回收,线程饱和时,直接丢弃最老的任务:

privateExecutorServiceexecutorServiceForTask(LongtaskId){ returnthis.executorServiceRegistry.computeIfAbsent(taskId,id->{ BasicThreadFactorythreadFactory=newBasicThreadFactory.Builder()//指定线程池名称.namingPattern("Async-Task-"+taskId+"-Thread-%d")//设置线程为后台线程.daemon(true).build();//线程池核心配置://1.每个线程池最多只有一个线程//2.线程空闲超过秒进行自动回收//3.直接使用交互器,线程空闲进行任务交互//4.使用指定的线程工厂,设置线性名称//5.线程池饱和,自动丢弃最老的任务returnnewThreadPoolExecutor(0,1,L,TimeUnit.SECONDS,newSynchronousQueue<>(),threadFactory,newThreadPoolExecutor.DiscardOldestPolicy());});}

       最后,在线程池中运行的Task如下:

privateclassTaskRunnerimplementsRunnable{ privatefinalDatenow=newDate();privatefinalLongtaskId;publicTaskRunner(LongtaskId){ this.taskId=taskId;}@Overridepublicvoidrun(){ //重新加载任务,保持最新的任务状态TaskDefinitionV2task=definitionV2Repository.findById(this.taskId).orElse(null);if(task!=null&&task.shouldRun(now)){ //运行任务runTask(task);//更新任务的下一次运行时间updateNextRunTime(task,now);}}}4.TaskScheduler配置方案

       该方案的核心为:绕过@Schedule注解,直接对Spring底层核心类TaskScheduler进行配置。

       TaskScheduler接口是Spring对调度任务的一个抽象,更是@Schedule背后默默的支持者,首先我们看下这个接口定义。

publicinterfaceTaskScheduler{ ScheduledFutureschedule(Runnabletask,Triggertrigger);ScheduledFutureschedule(Runnabletask,InstantstartTime);ScheduledFutureschedule(Runnabletask,DatestartTime);ScheduledFuturescheduleAtFixedRate(Runnabletask,InstantstartTime,Durationperiod);ScheduledFuturescheduleAtFixedRate(Runnabletask,DatestartTime,longperiod);ScheduledFuturescheduleAtFixedRate(Runnabletask,Durationperiod);ScheduledFuturescheduleAtFixedRate(Runnabletask,longperiod);ScheduledFuturescheduleWithFixedDelay(Runnabletask,InstantstartTime,Durationdelay);ScheduledFuturescheduleWithFixedDelay(Runnabletask,DatestartTime,longdelay);ScheduledFuturescheduleWithFixedDelay(Runnabletask,Durationdelay);ScheduledFuturescheduleWithFixedDelay(Runnabletask,longdelay);}

       满满的都是schedule接口,其他的比较简单就不过多叙述了,重点说下Trigger这个接口,首先看下这个接口的定义:

publicinterfaceTrigger{ DatenextExecutionTime(TriggerContexttriggerContext);}

       只有一个方法,获取下次执行的时间。在任务执行完成后,会调用Trigger的nextExecutionTime获取下一次运行时间,从而实现周期性调度。

       CronTrigger是Trigger的最常见实现,以linuxcrontab的方式配置调度任务,如:

scheduler.schedule(task,newCronTrigger("-**MON-FRI"));

       基础部分简单介绍到这,让我们看下数据库动态配置方案。

4.1数据库动态配置方案

       整体设计如下:

       仍旧是轮询数据库方式,详细流程如下:

       1.在应用中启动一个Schedule任务(每1秒调度一次),定时从数据库中获取所有任务;2.依次遍历任务,与内存中的TaskEntry(任务与状态)进行比对,动态的向TaskScheduler中添加或取消调度任务;3.由TaskScheduler负责实际的任务调度;

       核心代码如下:

@Scheduled(fixedDelay=,initialDelay=)publicvoidloadAndConfig(){ //加载所有的任务信息List<TaskDefinitionV3>tasks=repository.findAll();//遍历任务进行任务检查for(TaskDefinitionV3task:tasks){ //获取内存任务状态TaskEntrytaskEntry=this.taskEntry.computeIfAbsent(task.getId(),TaskEntry::new);if(task.isEnable()&&taskEntry.isStop()){ //任务为可用,运行状态为停止,则重新进行schedule注册ScheduledFuture<?>scheduledFuture=this.taskScheduler.scheduleWithFixedDelay(newTaskRunner(task),task.getDelay()*);taskEntry.setScheduledFuture(scheduledFuture);log.info("successtostartscheduletaskfor{ }",task);}elseif(task.isDisable()&&taskEntry.isRunning()){ //任务为禁用,运行状态为运行中,停止正在运行在任务taskEntry.stop();log.info("successtostopscheduletaskfor{ }",task);}}}

       核心辅助类:

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}0

       有没有发现,以上方案都有一个共同的缺陷:基于数据库轮询获取任务,加大了数据库压力。理论上,只有在配置发生变化时才有必要对任务进行更新,接下来让我们看下改进方案:基于配置中心的方案。

4.2配置中心通知方案

       整体设计如下:

       核心流程如下:

       1.应用启动时,从配置中心中获取调度的配置信息,并完成对TaskScheduler的配置;2.当配置发送变化时,配置中心会主动将配置推送到应用程序,应用程序在接收到变化通知时,动态的增加或取消调度任务;3.任务的实际调度仍旧由TaskScheduler完成。

       由于手底下没有配置中心,暂时没有coding,思路很简单,有条件的同学可以自行完成。

5.分布式环境下应用

       以上方案,都是在单机环境下运行,如果应用程序挂掉了,任务调度也就停止了,为了避免这种情况的发生,需要提升系统的可用性,实现冗余部署和自动化容灾。

       以上方案,如果部署多个节点会发生什么?是的,会出现任务被多次调度的问题,为了保障在同一时刻只有一个任务在运行,需要为任务增加一个排他锁。同时,由于排他锁的存在,当一个节点处问题后,另一个节点在调度时会自动获取锁,从而解系统的单点问题。

       为了简单,我们使用Redis的分布式锁。

5.1.环境搭建

       Redisson是Redis的一个富客户端,提供了很多高级的数据结构。本次,我们将使用RLock对应用进行保护。

       首先,在pom中引入RedissonStarter。

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}1

       然后,在application.properties文件中增加Redis配置,具体如下:

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}.2引入分布式锁

       最后,就可以直接使用分布式锁对任务执行进行保护了,代码如下:

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}3

       备注:

       Redis是典型的AP应用,而分布式锁严格意义上来说是CP。所以基于Redis的分布式锁只能使用在非严格环境中,比如我们的数据报表需求。如果设计金钱,需要使用CP实现,如Zookeeper或etcd等。

6.小结

       本文从Spring的Schedule出发,依次对数据库轮询方案、TaskScheduler配置方案进行详细讲解,以实现对调度任务的可配置化。最后,使用Redis分布式锁有效解决了分布式环境下任务重复调度和自动容灾问题。

       仍旧是那句话,架构设计没有更好,只有最适合。同学们可以根据自己的需求自取。

References

       [1]源码:/litao/books/tree/master/configurable-schedule

JobScheduler的使用和原理

        JobScheduler主要用于在未来某个时间下满足一定条件时触发执行某项任务的情况,涉及的条件可以是网络、电量、时间等,例如执行特定的网络、是否只在充电时执行任务等。

        JobScheduler类负责将应用需要执行的任务发送给框架,以备对该应用Job的调度,是一个系统服务,可以通过如下方式获取:

        JobInfo是传递给JobScheduler类的数据容器,它封装了针对调用应用程序调度任务所需的各种约束,也可以认为一个JobInfo对象对应一个任务,JobInfo对象通过JobInfo.Builder创建。它将作为参数传递给JobScheduler:

        JobInfo.Builder是JobInfo的一个内部类,用来创建JobInfo的Builder类。

        JobService是JobScheduler最终回调的端点,JobScheduler将会回调该类中的onStartJob()开始执行异步任务。它是一个继承于JobService的抽象类,做为系统回调执行任务内容的终端,JobScheduler框架将通过bindService()方式来启动该服务。因此,用户必须在应用程序中创建一个JobService的子类,并实现其onStartJob()等回调方法,以及在AndroidManifest.xml中对它授予如下权限:

        注意在AndroidManifest.xml中添加权限

        当任务开始时会执行onStartJob(JobParameters params)方法,如果返回值是false,则系统认为这个方法返回时,任务已经执行完毕。如果返回值是true,那么系统认为这个任务正要被执行,执行任务的重担就落在了你的肩上。当任务执行完毕时你需要调用jobFinished(JobParameters params, boolean needsRescheduled)来通知系统。

        当系统接收到一个取消请求时,系统会调用onStopJob(JobParameters params)方法取消正在等待执行的任务。很重要的一点是如果onStartJob(JobParameters params)返回false,那么系统假定在接收到一个取消请求时已经没有正在运行的任务。换句话说,onStopJob(JobParameters params)在这种情况下不会被调用。

        需要注意的是这个Job Service运行在主线程,这意味着你需要使用子线程,handler,或者一个异步任务来运行耗时的操作以防止阻塞主线程。

        Google官方的Sample: /googlearchive/android-JobScheduler

        JobScheduler是一个抽象类,它在系统框架的实现类是android.app.JobSchedulerImpl

        执行的入口是JobScheduler.scheduler,其实是调了JobSchedulerImpl中的schedule方法;然后再调了mBinder.schedule(job)。这个mBinder就是JobSchedulerService,通过Binder跨进程调用JobSchedulerService。

        最后调用到JobSchedulerService中的schedule方法:

        接着发送MSG_CHECK_JOB消息,消息处理的地方是

        接着执行JobHandler中的 maybeRunPendingJobsH 方法,处理相应的任务

        availableContext是JobServiceContext,即ServiceConnection,这个是进程间通讯ServiceConnection,通过调用availableContext.executeRunnableJob(nextPending)方法,会触发调用onServiceConnected,看到这里应该明白了,onServiceConnected方法中的service就是Jobservice,里面还用了WakeLock锁,防止手机休眠。

        接着,通过Handler发消息,调用了handleServiceBoundH()方法。

        从上面源码可以看出,最终是触发调用了JobService中的startJob方法。

        从源码看,设置的内容应用于 JobStatus ,例如网络限制

        而在JobSchedulerService类,相关的状态控制在其构造函数里:

        例如网络控制类ConnectivityControllerç±»

        当网络发生改变时,会调用updateTrackedJobs(userid)方法,在updateTrackedJobs方法中,会判断网络是否有改变,有改变的会调mStateChangedListener.onControllerStateChanged()方法;然后调用了JobSchedulerService类中onControllerStateChanged方法:

        接着也是处理MSG_CHECK_JOB 消息,和上文一样,最终触发调用了JobService中的startJob方法。

        JobSchedulerService是一个系统服务,即应该在SystemServer启动的。阅读SystemServer的源码:

        run 方法如下:

        接着看 startOtherServices()

        因此,在这里就启动了JobSchedulerService服务。

        1. android 性能优化JobScheduler使用及源码分析

        2. Android 9.0 JobScheduler(一) JobScheduler的使用

        3. Android 9.0 JobScheduler(二) JobScheduler框架结构简述及JobSchedulerService的启动

        4. Android 9.0 JobScheduler(三) 从Job的创建到执行

        5. Android 9.0 JobScheduler(四) Job约束条件的控制

        6. 理解JobScheduler机制

技术人生阅读源码——Quartz源码分析之任务的调度和执行

       Quartz源码分析:任务调度与执行剖析

       Quartz的调度器实例化时启动了调度线程QuartzSchedulerThread,它负责触发到达指定时间的任务。该线程通过`run`方法实现调度流程,包含三个主要阶段:获取到达触发时间的triggers、触发triggers、执行triggers对应的jobs。

       获取到达触发时间的triggers阶段,通过`JobStore`接口的`acquireNextTriggers`方法获取,由`RAMJobStore`实现具体逻辑。触发triggers阶段,调用`triggersFired`方法通知`JobStore`触发triggers,处理包括更新trigger状态与保存触发过程相关数据等操作。执行triggers对应jobs阶段,真正执行job任务,先构造job执行环境,然后在子线程中执行job。

       job执行环境通过`JobRunShell`提供,确保安全执行job,捕获异常,并在任务完成后根据`completion code`更新trigger。job执行环境包含job对象、trigger对象、触发时间、上一次触发时间与下一次触发时间等数据。Quartz通过线程池提供多线程服务,使用`SimpleThreadPool`实例化`WorkerThread`来执行job任务,最终调用`Job`的`execute`方法实现业务逻辑。

       综上所述,Quartz通过精心设计的线程调度与执行流程,确保了任务的高效与稳定执行,展示了其强大的任务管理能力。