【优志愿网站源码】【赠源码指标】【付费源码设计】crontab源码

时间:2024-12-23 23:13:05 编辑:php客服源码免费 来源:指标源码改EA

1.require和include的区别
2.Django实现crontab远程任务管理系统
3.盲盒商城源码开源完整版附搭建教程UNIAPP·HashMart
4.可动态配置的Schedule设计
5.不提你可能不知道,spring定时任务的数字星期域不符合常规的cron定义

crontab源码

require和include的区别

       nclude与require的区别

       PHP中的require,require_once,include,include_once的区别

        “include”与“required”的作用都是相同的,唯一不同的是PHP在遇到“include”命令时,它就必须重新解释一次。如果在同一个PHP网页中出现次“include”命令时,它便会被重新解释次。不过当PHP遇到“require”命令时,不管它在同一个PHP网页中出现过几次,PHP只会解释一次而已。

        “require”的工作方式是为了让PHP程序得到更高的效率,所以当它在同一个PHP网页中解释过一次后,第二次出现便不会再解释,这是它的优点。不过严格来说,这也是它的唯一 的缺点,因为它不会重复解释引入的文件,所以当PHP网页中使用循环或条件语句来引入文件时,“require”则不会做任何的改变。当有类似这样的情形时,就必须使用“include”命令来引入 文件了。

        当PHP遇到一个利用“include”方式引入的文件,它就会解释一次;遇到第二次时,PHP还是会重新解释一次。与“require”相比,“include”的执行效率则会下降许多;而且当引入文件中包含了用户自定义的函数时,PHP在解释的过程中会发生函数重复定义的问题。不过“include”也不是没有优点的,因为在PHP网页中,它会每遇到一次“include”命令就会重复解释一次,所以非常适合使用在循环或条件判断的语句里。

        “include_once()”函数和“require_once()” 函数功能完全相同,会先检查目标档案的内容是不是在之前就已经导入过了,如果是的话,便不会再次重复导入同样的内容。

       çŽ°åœ¨æ¥è¯´include和require的区别:

       require()函数包含进来的内容被当成当前文件的一个组成部分,所以当包含进来的文件有语法错误或者文件不存在的时候,那当前文件的PHP脚本都不再执行. include()函数相当于指定这个文件的路径,当被包含的文件有错时,不会影响到本身的程序运行.

       include函数可以进行判断是否包含,而require则是不管任何情况都包含进来.所以这点值得注意!

       å»ºè®®å¤§å®¶åœ¨åŒ…含动态文件,也就是有变量,函数,已经类的时候用include.不过前段时间有人在分析两个函数的执行效率.这个我没自己测试过,等亲自测试了再进行补充

       www.w3school.com

       é€šè¿‡ include() 或 require() 函数,您可以在服务器执行 PHP 文件之前在该文件中插入一个文件的内容。除了它们处理错误的方式不同之外,这两个函数在其他方面都是相同的。include() 函数会生成一个警告(但是脚本会继续执行),而 require() 函数会生成一个致命错误(fatal error)(在错误发生后脚本会停止执行)。

       è¯¦ç»†ä»‹ç»

       äºŽPHP具有快速、可靠、跨平台应用、源代码开放等特点,使得PHP成为最受欢迎的服务器端Script语言之一。我根据自己在工作中体会到的,向大家介绍PHP使用的心得,希望对大家有所帮助。

        利用PHP的Include files维护你的网站

        不管你所开发的网站的规模是大是小,你都应该要认识到重复使用程序代码的重要性,不论你重复使用的是 PHP 程序或者是 HTML 原始码。举个例子来说,网站页尾的版权宣告至少每年都得修改一次,如果你的网站有许多个页面,该怎么办呢?动手一个一个修改这些页面肯定是一件头痛的事 情。通过 PHP 我们可以用几个不同的方式来重复使用程序代码。要使用哪些函数端视你要重复使用的是怎样的内容而定。

        这些主要的函数包括:

        * include() 与 include_once()

        * require() 与 require_once()

        1.include() 函数会将指定的档案读入并且执行里面的程序。

        例如:include('/home/me/myfile');

        被导入的档案中的程序代码都会被执行,而且这些程序在执行的时候会拥有和源文件中呼叫到 include() 函数的位置相同的变量范围(variable scope)。你可以导入同一个服务器中的静态档案,甚至可以通过合并使用 include() 与 fopen() 函数来导入其它服务器上面的档案。

        2.include_once()函数的作用和 include() 是几乎相同的

        唯一的差别在于 include_once() 函数会先检查要导入的档案是不是已经在该程序中的其它地方被导入过了,如果有的话就不会再次重复导入该档案(这项功能有时候是很重要的,比方说要导入的档 案里面宣告了一些你自行定义好的函数,那么如果在同一个程序重复导入这个档案,在第二次导入的时候便会发生错误讯息,因为 PHP 不允许相同名称的函数被重复宣告第二次)。

        3.require()函数会将目标档案的内容读入,并且把自己本身代换成这些读入的内容。

        这个读入并且代换的动作是在 PHP 引擎编译你的程序代码的时候发生的,而不是发生在 PHP 引擎开始执行编译好的程序代码的时候(PHP 3.0 引擎的工作方式是编译一行执行一行,但是到了 PHP 4.0 就有所改变了,PHP 4.0 是先把整个程序代码全部编译完成后,再将这些编译好的程序代码一次执行完毕,在编译的过程中不会执行任何程序代码)。require() 通常来导入静态的内容,而 include() 则适合用来导入动态的程序代码。

        4.如同 include_once()函数,require_once() 函数会先检查目标档案的内容是不是在之前就已经导入过了,如果是的话,便不会再次重复导入同样的内容。

        我个人习惯使用 require() 函数来导入版权宣告(copyrights),静态文字或其它本身不含有变量,

       æˆ–者本身需要倚赖其它执行过的程序才能正确执行的程序代码。例如:

        <HTML>

        <HEAD><TITLE>网页标题</TITLE></HEAD> <BODY> [一堆内容] <?

        // 导入版权宣告文字

        require('/home/me/mycopyright'); ?>

        </BODY></HTML>

        另一方面,我通常在程序的开头使用 include() 函数来导入一些函式库或者类似的程序代码: <?

        // 导入我的函式库

        include('/home/me/myfunctions');

        // 利用之前导入的函式库里面定义好的 PHP 函数执行一些功能?> <HTML>

        <HEAD><TITLE>网页标题</TITLE></HEAD> <BODY> [一堆内容] </BODY> </HTML>

        接下来你可能会问这第一个挺符合逻辑的问题:「这些被导入的档案要放在哪儿呢?」简短的答案是:「放在服务器档案系统里的任何地方都行。」然而,要留意的 是如果被导入的档案除了单纯的程序代码片段以外还包含了一些敏感资料,例如连结数据库系统要用到的帐号和密码,那么建议你不要把这些档案放在 Web 服务器的文件根目录之下,因为那样的话他人便可以很容易地窃取到这些资料了。

        你可以将这些被包含的档案放在系统的任何一个目录里面,唯一的条件是 PHP 本身用来执行的身分(www,nobody 或者其它身分)必须要有足够的权限能够读取这些档案就可以了。这些档案的扩展名也可以任意取,甚至没有附档名也无所谓。

        善用include()和 require()来将网站里面经常需要变动的共享内容做合理的分割,在更新网站内容的时候将会容易进行得多。

        利用PHP来维护档案系统

        PHP 提供了很多与档案系统相关的函数,让我们不仅可以开启档案,还能够显示目录的内容,搬移档案的位置以及其它更多功能。有的朋友甚至写了能够通过浏览器来管理档案内容的 PHP 程序。

        在开始介绍 PHP 的档案系统相关功能之前,我们要先理清一件事情:在 Windows操作系统里

       é¢ï¼Œæ¡£æ¡ˆè·¯å¾„可以使用斜线(/)或者反斜线(\)来表示,但是在其它操作系统里面我们只会使用到斜线。为了保持统一性,下面的例 子里面的档案路径都是使用斜线。

        下面的例子程序我将教大家基本的目录内容显示功能,每个步骤都有批注,请直接阅读。

        <? /* $dir_name 这个变量的值是你想要读取的目录的完整路径 */ $dir_name = "/home/me/";

        /* opendir()函数会开启某个目录,并且传回一个参考值(handle)让我们可以用来在程序中参照到该目录 */

        $dir = opendir($dir_name);

        /* 开始建立一个字符串,这个字符串包含了 HTML 的列表卷标,用来显示目录中的文件名称。 */

        $file_list = "<ul>";

        /* 使用一个 while 循环叙述将前面开启的目录中的档案全部读取一遍。如果读取到的档名不是「.」或者「..」,就把该档名写入前面提到的字符串里面去。 */ while ($file_name = readdir($dir)) {

        if (($file_name != ".") && ($file_name != "..")) { $file_list .= "<li>$file_name"; } }

        /* 替 HTML 列表卷标加上结尾 */ $file_list .= "</ul>";

        /* 关闭之前开启的目录并且结束这段 PHP 程序 */ closedir($dir); ?>

        <!-- HTML原始码从这里开始 --> <HTML> <HEAD>

        </HEAD> <BODY>

        <!-- 使用 PHP 程序来将我们所读取的目录名称显示在页面上 --> <P>Files in: <? echo "$dir_name"; ?></p>

        <!-- 使用 PHP 程序将该目录中读取到的文件名显示在页面上 --> <? echo "$file_list"; ?> </BODY> </HTML>

        经过上面几步,你已经成功把某个目录中的文件名称显示在网页上了。但你要记住一点:要读取某个目录或者档案(读取档案内容的做法稍后会介绍),PHP 本身执行所用的身分必须至少拥有该目录或者档案的读取权限才行,否则系统会显示权限不足的错误讯息。

        下一个例子我将教大家如何复制一个档案:

        <? /* 变量$orginal储存源文件的完整路径,变量$copied储存复制过去的新档案的完整路径 */ $original = "/home/me/mydatabasedump"; $copied = "/archive/mydatabasedumo_";

        /* 呼叫 copy() 函数把档案从原始位置复制一份到新的位置去。如果无法复制,那么便终止程序的执行并且显示错误讯息。 */

        @copy($original, $copied) or die("无法复制档案。"); ?>

        上面的例子程序可以用来扩充成为一个档案备份系统程序。当这个程序执行的时候,它会将数据库的数据文件复制到其它目录下面做为备份之用。只要修改系统的排 程档案内容(crontab),我们便可以让这个程序自动在每天的固定时间执行一次,达到系统自动备份,不需要人工手动执行。

        如果你的系统上面有安装 Lynx 软件(Lynx 是一种纯文字的 Web 浏览器)的话,你可以在系统排程档案里面加入下面这笔记录来让系统在固定时间自动激活 Lynx 并且呼叫我们之前写好的 PHP 备份程序。当 Lynx 呼叫(浏览)我们的 PHP 程序的时候,该程序就会被执行,并且产生备份文件。下面这个例子教你如何在每天早晨五点钟执行我们的备份程序,并且在执行完以后自动将 Lynx 程序关闭:

       æ¥è‡ªï¼šé—«å¿—飞 > 《php》

       ä¸Šä¸€ç¯‡ï¼šphp 导出excel (html)

       ä¸‹ä¸€ç¯‡ï¼šWindows版本Apache+php的Xhprof应用——1

       è½¬è—åˆ°æˆ‘的图书馆

       çŒ®èŠ±(0)

       åˆ†äº«åˆ°å¾®ä¿¡

       åˆ†äº«ï¼š

       ç±»ä¼¼æ–‡ç« 

       æ›´å¤š

       PHP之PHP文件引用详解

       æ¯”较require(),include(),require_once(...

       php header函数使用要点

       include 和 include_once 有什么分别?r...

       åå¤©å­¦ä¼šPHP/第六天:PHP日期、引用

       PHP中file_exists与is_file,is_dir的区别....

       php 删除目录下N分钟前创建的所有文件

       è¿‡æ»¤å±é™©html代码的php自定义函数

       çƒ­é—¨æŽ¨å¹¿

       çŒœä½ å–œæ¬¢

       æœ€ç¾Žé›ªæ™¯æ¬£èµâ€”—你那里下雪了吗?

       å¹¿å‘Šè¥é”€å¦‚千军万马过独木桥 成功质...

       æ€ä¹¡æœ€æ˜¯è¯—行,给漂泊的游子

       ä¸ºäº†ä¸å¾—癌症,今天就要做

       åˆæ‹çš„地方

       ç§åƒå¾—起的抗衰老食物

       ä¸–界十大神秘"鬼船"有什么神秘故事

       å…¬å…±åŸºç¡€çŸ¥è¯†é¢˜

       è€å¸ˆå¸¸ç”¨æ•™å­¦ç½‘站账号密码大集合

       æ²¡æœ‰æ‰«æä»ªï¼Œæ€Žä¹ˆåŠžï¼Ÿç”¨ä»€ä¹ˆä»£æ›¿ï¼Ÿ

       å‘表评论:

       æ‚¨å¥½ï¼Œè¯· 登录 或者 注册 后再进行评论

       å…¶å®ƒå¸å·ç™»å½•ï¼š

       æœ€æ–°æ–‡ç« 

       æ·±å…¥ç†è§£php底层:php生命周期

       é€šè¿‡virtualbox最小化安装centos 6.3...

       debian下控制台和VI彩色显示&ssh

       Ubuntu下使用SVN

       sources镜像:sources.list.wheezy.de...

       nginx.conf 配置lnmp

       æ›´å¤š

       çƒ­é—¨æ–‡ç« 

       æ²¡è§è¿‡ç§èŠ±å·çš„做法吧&别错过学习的...

       â€œä¸­å›½å¼xx”让全世界哭笑不得

       å°å­¦è‹±è¯­è¾…导全套视频教程【珍藏版】

       æˆ‘ 家 宝 贝 女 儿[5]

       ã€ä»Žâ€œå°ä¸€â€œè‡³â€œé«˜ä¸‰â€œå„年级对孩子...

       çŒæ±¤åŒ…的皮怎么做又薄又透,能当气球吹

       èœ‚蜜加肉桂 ---- 令人叹为观止的神奇

       å¾·å›½åˆ¶é€ å’Œä¸­å›½åˆ¶é€ ç©¶ç«Ÿä¸åŒåœ¨å“ª

       å¥³äººçš„苦处:写的非常好——

       å¤–媒一则漫画揭示中美博弈(很喜感)

       ç™½é…’鲜姜云南白药粉治秃顶脱发效果神奇

       äººè¦é•¿ç”Ÿï¼Œè‚ è¦å¸¸æ¸…

       æ›´å¤š>>

       å…³é—­

       å…³é—­

Django实现crontab远程任务管理系统

       在之前的文章中,我们已经探讨了如何使用 django-crontab 和 apscheduler 在Django应用内部管理定时任务,这些模块主要用于处理应用自身的任务调度。

       然而,本文将转向一个不同的优志愿网站源码场景,类似于Java的xxl-job,我们构建了一个系统,能够通过Ansible API,远程管理不同Java项目中Task的定时任务。这个系统是ansible cron模块的可视化界面,允许你便捷地在Django后台添加、修改和删除Linux主机上的crontab任务。

       核心技术实现涉及创建一个crontab模型,并将其集成到Django Admin中。每当模型发生变化时,会触发post_save信号,进而通过celery执行Task。这个Task调用ansible-runner的playbook接口,将crontab命令发送到指定主机。

       模型设计和celery task的ansible-playbook执行是关键部分。在编写Task函数时,我们注意到增加了一个未实际使用的update_time参数,以确保每次更新都会生成新的Task实例。同时,使用mark_safe函数处理crontab命令中的特殊字符,render_to_string用于根据模型数据动态生成playbook模板,os.environ设置ansible的环境变量。

       配置celery和信号处理,包括celery任务注册、异步任务日志独立存放以及信号机制的理解,都构成了技术栈的一部分。同时,我们还讨论了logging配置,赠源码指标以及在Django Admin后台记录操作的问题,特别是关于用户身份识别的挑战。

       源代码已发布在gitee上,dj_cronjobs[6],并提供了详细的Readme.md指南供读者参考。如果你觉得这个系统有用,请通过我的个人公众号(搜索全栈运维 或者 DailyJobOps)获取更多信息,也可以直接在公众号中找到Django获取当前登录用户的方法[5]。

       相关链接如下:

       [6] dj_cronjobs: gitee.com/colin/dja...

盲盒商城源码开源完整版附搭建教程UNIAPP·HashMart

       确保环境准备:推荐使用宝塔面板搭建,安装步骤为:访问 bt.cn/new/download.html 下载宝塔安装文件,按照提示完成安装。确保服务器环境为Linux CentOS,安装PHP扩展fileinfo和redis。域名解析应指向服务器IP。

       准备前端环境:安装HBuilder X 3.7.6和微信开发者工具。Node.js版本需更新至v..0以上。

       下载并安装源码:从码云下载源码至api目录,解压后上传至服务器。通过浏览器访问安装页面,按照提示填写协议、数据库信息,并检查安装。安装完成后,删除安装目录,使用管理员密码登录后台。

       配置定时任务:为自动处理超时订单,需在supervisor管理器中设置定时任务,以确保ThinkPHP的crontab定时运行。

       小程序编译:使用HBuilderX打开uniapp文件夹,配置微信小程序AppID和接口地址。确保uniCloud文件夹在运行时可用。

       App编译设置:为uni-app应用设置标识和图标。配置云函数、付费源码设计云空间关联,完成后可发行并发布到App Store或Google Play。

       基础配置检查:仔细设置存储引擎、支付参数、小程序和uniapp配置。确保所有配置正确无误,以保证盲盒商城系统正常运行。

可动态配置的Schedule设计

       1.背景

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

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

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

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

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

       1.SpringSchedule配置和使用。首先我们将介绍Demo的骨架,并基于Spring-Boot完成Schedule的配置;2.数据库定时轮询方案。使用SpringSchedule定时轮询数据库,并执行相应任务。传统java源码在执行任务策略中,我们将尝试同步和异步执行两种方案,并对其优缺点进行分析;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

不提你可能不知道,spring定时任务的数字星期域不符合常规的cron定义

       了解Spring定时任务的基本配置后,许多开发者会发现其与cron表达式的某些不寻常之处。本文将深入探讨Spring定时任务的数字星期域与传统cron定义之间的差异。

       在配置Spring定时任务时,使用@Scheduled(cron = "* * 1 * * *")可以轻松实现每天1点定时执行任务。但若尝试构建特定于星期一中午点的定时任务,您会发现cron表达式的应用与预期不符。

       在cron表达式中,星期一对应的数字是2,表示从星期天(数字1)开始的一周循环。然而,当将此类cron表达式应用于Spring定时任务时,任务实际上会在下一次星期二的同一时间执行,而非预期的星期一。

       这一现象同样存在于直接使用Spring的CronTask类,并传递cron表达式时。究其原因,Spring内部源码的处理逻辑导致了这一不一致性。在生成CronTrigger时,解析cron表达式的过程存在差异。

       解析过程涉及对数字星期域进行特殊转换,将其从英文缩写转换为数字,并对特定值进行处理。其中的关键在于对daysOfWeek位数组的操作,该数组用于存储解析后的星期信息。

       具体而言,解析过程首先将英文缩写转换为对应的数字表示,然后将数字域中"?"替换为"*",接着使用基础解析算法处理。最后,对daysOfWeek数组的第0位和第7位进行逻辑或操作,并将结果保存在第0位,同时清除第7位。这一处理方式导致了数字星期域与传统cron表达式之间的一天偏差。

       尽管如此,网络上关于Spring定时任务的教程和文章多聚焦于cron表达式的基础解释,较少提及此类问题的详细原因。然而,解决方法相对简单且有效:在cron表达式中使用英文缩写的星期表示,而非数字。这样做能够避免因数字转换导致的定时任务执行时间偏移。

       春代码设计人员选择这种处理方式可能与与Crontab中的cron表达式格式以及Linux计划任务的兼容性有关。Crontab采用0-7的数字表示星期,同时其格式在秒域的处理上与cron表达式有所不同。

       综上所述,对于在Spring中使用cron表达式配置定时任务的场景,推荐使用英文缩写来表示星期域。这样可以确保任务执行时间的准确性,并避免由于数字转换而导致的时间偏移。