首先说说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
基本格式 :
* * * * * command
分 时 日 月 周 命令
第1列表示分钟1~59 每分钟用*或者 */1表示
第2列表示小时1~23(0表示0点)
第3列表示日期1~31
第4列表示月份1~12
第5列标识号星期0~6(0表示星期天)
第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-结束