两则预防crontab重复执行任务策略

首先说说cron,它是一个linux下的定时执行工具。根用户以外的用户可以使用 crontab 工具来配置 cron 任务。所有用户定义的 crontab 都被保存在/var/spool/cron 目录中,并使用创建它们的用户身份来执行。要以某用户身份创建一个 crontab 项目,登录为该用户,然后键入 crontab -e 命令来编辑该用户的 crontab。该文件使用的格式和 /etc/crontab 相同。当对 crontab 所做的改变被保存后,该 crontab 文件就会根据该用户名被保存,并写入文件 /var/spool/cron/username 中。cron 守护进程每分钟都检查 /etc/crontab 文件、etc/cron.d/ 目录、以及 /var/spool/cron 目录中的改变。如果发现了改变,它们就会被载入内存。这样,当某个 crontab 文件改变后就不必重新启动守护进程了。

centos7.5安装crontab

yum intall -y cronie

  1. 基本格式 :
  2. * * * * * command
  3. 分 时 日 月 周 命令
  4. 第1列表示分钟1~59 每分钟用*或者 */1表示
  5. 第2列表示小时1~23(0表示0点)
  6. 第3列表示日期1~31
  7. 第4列表示月份1~12
  8. 第5列标识号星期0~6(0表示星期天)
  9. 第6列要运行的命令

crontab 执行 php 脚本

linux 下的 crontab 定时任务服务,可以用来定时运行脚本。工作中经常会用到这样的服务,使用起来比较简单。

在目前的 CentOS 7(或 RHEL 7)系统中,依然可以使用 service 指令.例如,

[root@localhost ~]# service network restart
Restarting network (via systemctl): [OK ]
[root@localhost ~]# service httpd restart
Redirecting to /bin/systemctl restart httpd.service
[root@localhost ~]# service sshd restart
Redirecting to /bin/systemctl restart sshd.service

但是系统会自动重定向该指令到新的指令 /bin/systemctl 来执行,并给出提示.

启动服务:

systemctl start httpd

停止服务:

systemctl stop httpd

重启服务(先停止,后启动):

systemctl restart httpd

重新加载(使用新的配置文件):

systemctl reload httpd

显示服务状态:

systemctl status httpd

与此同时,之前用于设定系统启动时自动运行某服务的指令 chkconfig 也改了,还是用 systemctl.

chkconfig service on

改成了,

systemctl enablehttpd

扫描改动过的服务

systemctl daemon-reload
chkconfig service off

改成了,

systemctl disable httpd

检查服务状态的

chkconfig service

改成了,

systemctl is-enabled httpd

列举出所有服务的指令,

chkconfig –list

改成了,

systemctl list-unit-files --type=service

以前能指定服务 runlevel 的 –levels 也没有了.慢慢适应吧.

参 数:
-e 编辑该用户的计时器设置。
-l 列出该用户的计时器设置。
-r 删除该用户的计时器设置。
-u<用户名称> 指定要设定计时器的用户名称。

crontab 格式:

基本格式 :

分钟 小时 日 月 星期 命令

* * * * * *

第1列表示分钟1~59 每分钟用*或者 */1表示
第2列表示小时1~23(0表示0点)
第3列表示日期1~31
第4列 表示月份1~12
第5列标识号星期0~6(0表示星期天)
第6列要运行的命令

记住几个特殊符号的含义:
“*”代表取值范围内的数字,
“/”代表”每”,
“-”代表从某个数字到某个数字,
“,”分开几个离散的数字

预防crontab重复执行任

最近在工作中经常会用到定时任务,发现当我们的脚步的执行时间(假设:130s)大于定时任务的设定时间(假设:1分钟)时,定时任务会重复开始执行,即上次的任务还没有执行完,下次的任务的又开始执行。往往执行的脚本里的资源是不允许同时两个脚本同时共享资源,即保证操作的原子性。这样会造成执行出错,下面我们来验证一下。

以下是一个测试的 php 脚本,该脚本执行一次需要 130s

<?php
$time = time();$id = uniqid(); //一次执行的唯一标示 
file_put_contents('/home/phachon/cron/test.log', "id: ".$id." 时间:".date('Y-m-d H:i:s', $time)."-开始\n", FILE_APPEND);
while(time() - $time < 130) {
 } 
file_put_contents('/home/phachon/cron/test.log', "id: ".$id." 时间:".date('Y-m-d H:i:s', time())."-结束\n", FILE_APPEND);

然后添加定时任务,每分钟(60s)执行一次

*/1 * * * * php /home/phachon/cron/test.php

过一段时间后,查看日志:

id: 57bbcd4d10262 时间:2016-08-23 12:13:01-开始
id: 57bbcd890e7f7 时间:2016-08-23 12:14:01-开始
id: 57bbcdc510685 时间:2016-08-23 12:15:01-开始
id: 57bbcd4d10262 时间:2016-08-23 12:15:11-结束
id: 57bbce010a78d 时间:2016-08-23 12:16:01-开始
id: 57bbcd890e7f7 时间:2016-08-23 12:16:11-结束
id: 57bbce3d0f68e 时间:2016-08-23 12:17:01-开始
id: 57bbcdc510685 时间:2016-08-23 12:17:11-结束
id: 57bbce790d90f 时间:2016-08-23 12:18:01-开始
id: 57bbce010a78d 时间:2016-08-23 12:18:11-结束
id: 57bbceb50eef8 时间:2016-08-23 12:19:01-开始
id: 57bbce3d0f68e 时间:2016-08-23 12:19:11-结束
id: 57bbce790d90f 时间:2016-08-23 12:20:11-结束
id: 57bbceb50eef8 时间:2016-08-23 12:21:11-结束

分析日志我们会发现 id = 57bbcd4d10262 的任务在 12:13:01 开始,但是还没有结束的时候,id=57bbcd890e7f7 和 id=57bbcdc510685 的任务就已经开始了,这样明显存在问题。我们想要的是每次单独执行完后,下一个执行开始:

id: 57bbcd4d10262 时间:2016-08-23 12:13:01-开始
id: 57bbcd4d10262 时间:2016-08-23 12:15:11-结束
id: 57bbcd890e7f7 时间:2016-08-23 12:14:01-开始
id: 57bbcd890e7f7 时间:2016-08-23 12:16:11-结束

解决办法

1,利用临时文件

思路很简单,在执行文件的开头先判断是否有一个 test.lock 的文件,如果有 test.lock 文件,则 exit(),如果没有的话,创建 test.lock 文件,然后执行脚本文件,执行完毕删除 test.lock;
实现后代码:

 <?php
   $time = time();
   $id = uniqid();
   $lock = '/home/phachon/cron/lock/test.lock';
   if(file_exists($lock)) {
       exit('no');
   }
   touch($lock);

   file_put_contents('/home/phachon/cron/test2.log', "id: ".$id." 时间:".date('Y-m-d H:i:s', $time)."-开始\n", FILE_APPEND);
   while(time() - $time < 130) {

   }

   file_put_contents('/home/phachon/cron/test2.log', "id: ".$id." 时间:".date('Y-m-d H:i:s', time())."-结束\n", FILE_APPEND);
   unlink($lock);

查看日志如下:

 id: 57bbdd3d6b5e8 时间:2016-08-23 13:21:01-开始
 id: 57bbdd3d6b5e8 时间:2016-08-23 13:23:11-结束
 id: 57bbddf10ecb9 时间:2016-08-23 13:24:01-开始
 id: 57bbddf10ecb9 时间:2016-08-23 13:26:11-结束

2,利用脚本加锁

思路和第一种方式类似,只是不是用文件判断的方式,而是给文件加锁的方式

实现代码:

<?php
$fp = fopen("/tmp/lock.txt", "w+");
// 进行排它型锁定
if (flock($fp, LOCK_EX | LOCK_NB)) {
   //执行任务
   run(); 
   // 释放锁定
   flock($fp, LOCK_UN); 
} else {
   echo "文件被锁定";
}
fclose($fp);
?>

第一种和第二种方法本质思路一样,确实也解决了问题,但是这样需要加代码在我们的脚本里,而且,这样其实 crontab 服务还是多了很多不必要的执行,浪费资源。
我们需要找到更加好的方法,在执行代码前就已经判断是否可以执行脚本。

对子任务加锁(如果有)

mysql的innodb update操作是行锁的,可以利用这点对子任务加锁,一条数据代表一个子任务
这样可以更西粒度的控制一个子任务同一时刻只有一个进程在执行,同时还可以开启多个进程。

 

function run($procid=0)//人为标识的进程id
{
$procNum = 10;//假设开10个进程


$data = select(..) from .. where id%$procNum=$procid;
foreach($data as $row){
exec($row);
}



}


function exec($val)
{
//lock
if(model()->lock($val['id'])) //执行一条update table set status='lock' where id=12312 and status='unlock';
return;
...run..
model()->succ($val['id']);

}

crontab脚本

​0* * * * /proc id=0

1* * * * /proc id=1

2* * * * /proc id=2

9* * * * /proc id=9

3,使用linux flock 文件锁实现任务锁定,解决冲突

利用 flock(FreeBSD lockf,CentOS下为 flock),在脚本执行前先检测能否获取某个文件锁,以防止脚本运行冲突。

格式:

 flock [-sxun][-w #] fd#
 flock [-sxon][-w #] file [-c] command

选项:

 -s, --shared:    获得一个共享锁 #共享锁,在定向为某文件的FD上设置共享锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置独占锁的请求失败,而其他进程试图在定向为此文件的FD上设置共享锁的请求会成功
 -x, --exclusive: 获得一个独占锁 #独占或排他锁,在定向为某文件的FD上设置独占锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置共享锁或独占锁都会失败。只要未设置-s参数,此参数默认被设置
 -u, --unlock:    移除一个锁,脚本执行完会自动丢弃锁 #手动解锁,一般情况不必须,当FD关闭时,系统会自动解锁,此参数用于脚本命令一部分需要异步执行,一部分可以同步执行的情况
 -n, --nonblock:  如果没有立即获得锁,直接失败而不是等待 #为非阻塞模式,当试图设置锁失败,采用非阻塞模式,直接返回1,
 -w, --timeout:   如果没有立即获得锁,等待指定时间 #设置阻塞超时,当超过设置的秒数,就跳出阻塞,返回1
 -o, --close:     在运行命令前关闭文件的描述符号。用于如果命令产生子进程时会不受锁的管控 
 -c, --command:   在shell中运行一个单独的命令 
 -h, --help       显示帮助 
 -V, --version:   显示版本

锁类型:

文件锁使用独占锁,非阻塞模式 如果锁定则失败不等待。参数为-xn

*/1 * * * *  flock -xn /tmp/test.lock -c 'php /home/phachon/cron/test.php' >> /home/phachon/cron/cron.log'

阻塞模式

*/1 * * * *  flock -x /tmp/test.lock -c 'php /home/phachon/cron/test.php' >> /home/phachon/cron/cron.log'

日志如下:

id: 57bbf255e4b2b 时间:2016-08-23 14:51:01-开始
id: 57bbf255e4b2b 时间:2016-08-23 14:53:11-结束
id: 57bbf3090eca0 时间:2016-08-23 14:54:01-开始
id: 57bbf3090eca0 时间:2016-08-23 14:56:11-结束

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code