magento — 计划任务(cron job)在magento中的作用和使用教程

计划任务(cron job)是Magento系统中一个很有用甚至可以说必须使用的功能,而最近在网上跟人聊天发现很多Magento的使用者不知道这个功能或者知道却认为可有可无,我在这里建议各位Magento的使用者,把这个功能使用起来吧。
在之前关于Newsletter的那篇博文我有讲到,要使新闻能按设定的时间自动发送,就必须设置好计划任务(cron job)。计划任务(cron job)简单的来说就是在系统中每隔一段时间来重复操作一件事,比如每隔多少时间清理日志,比如每天提交Sitemap给google等等。
而要设置计划任务(cron job),需要在网站运行的服务器(以linux为例)写一段脚本来定时调用网站根目录下的cron.PHP文件。下面是我自己使用的服务器的设置方式:

Thank you for reading this post, don't forget to subscribe!
crontab */5 * * * * /usr/bin/php -f /home/yourdomainname/public_html/cron.php

当然,首先你的服务器得支持cron job你才能使用crontab 这个命令,设置完的效果是服务器每隔5分钟运行一次你的网站根目录下的cron.php文件,cron.php运行时会去检查计划任务时间表中哪些任务的预定时间已经到了,如果到了就立即执行预定的任务,比如提交sitemap,并同时根据各个任务的配置文件或预先设定的任务(特指newsletter)生成新的计划任务时间表。储存这些数据的是数据库中的cron_schedule表,进这个表你会看到一个计划任务的列表

 

要让某个功能按时运行需要在对应的config.xml中设置计划任务,指定要运行的某个类的某个方法,同样以google sitemap为例

 

<crontab>  
       <jobs>  
           <sitemap_generate>  
               <run>  
                   <model>sitemap/observer::scheduledGenerateSitemaps</model>  
               </run>  
           </sitemap_generate>  
       </jobs>  
   </crontab>

你可以在Sitemap/Model/Observer.php文件中找到scheduledGenerateSitemaps这个方法,这个方法的作用就是向google提交sitemap。同理当你自己的模块有需要定时运行的功能是,可以参照sitemap在自己的config.xml配置要执行的方法。

Magento系统在你安装完毕后就已经自带了不少计划任务,在后台可以看到一些对计划任务的配置项,以sitemap为例

 

 

 

可以选择是否开启sitemap提交并选择提交的频率。

 

以1.4为例,Magento自带的计划任务包括刷新分类索引,应用价格规则,更新汇率,清理日志,发送Newsletter,发送商品提醒(价格变动和库存变化)和提交google sitemap等等。

 

 

所以下次再发现功能不能用比如设置的价格规则第二天失效了等,想一下自己是否忘了给网站设置了计划任务(cron job)。


设置计划任务,定时运行脚本

修改模块的配置文件config.xml

<crontab>
       <jobs>
           <计划任务标识>
               <schedule><cron_expr>0 */1 * * *</cron_expr></schedule>   <!--定时1分钟-->
               <run><model>你想要运行的脚本文件</model></run>
           </计划任务标识>
       </jobs>
 </crontab>

所有的计划任务存在cron_schedule表中


Magento计划任务详解

网站根目录下有一个shell脚本:

#!/bin/sh
# location of the php binary
if [ ! "$1" = "" ] ; then
CRONSCRIPT=$1
else
CRONSCRIPT=cron.php
fi
 
PHP_BIN=`which php`
 
# absolute path to magento installation
INSTALLDIR=`echo $0 | sed 's/cron\.sh//g'`
 
#prepend the intallation path if not given an absolute path
if [ "$INSTALLDIR" != "" -a "`expr index $CRONSCRIPT /`" != "1" ];then
    if ! ps auxwww | grep "$INSTALLDIR""$CRONSCRIPT" | grep -v grep 1>/dev/null 2>/dev/null ; then
    $PHP_BIN "$INSTALLDIR""$CRONSCRIPT" &
    fi
else
    if  ! ps auxwww | grep " $CRONSCRIPT" | grep -v grep | grep -v cron.sh 1>/dev/null 2>/dev/null ; then
        $PHP_BIN $CRONSCRIPT &
    fi
fi

$1引用第一参数,如果没有指定就是cron.php,然后从$0获取绝对路径(比如直接执行/www/magento/public_html/cron.sh,那么去掉cron.sh后得到/www/magento/public_html/),expr表示对后面的表达式求值,index表示字符串2在字符串1中的位置(位置从1开始),没有就是0,这里就是判断字符串是不是以斜杆开头。if语句可以确保当前没有运行将要运行的PHP脚本(如果还在运行说明上传运行还没有退出)。

从这个shell脚本可以看出,要运行脚本可以如下写:

/www/magento/public_html/cron.sh
/www/magento/public_html/cron.sh cron.php        #跟第一个类似 
/www/magento/public_html/cron.sh /other/path/**.php  #运行其它PHP脚本

另外,脚本中通过which来找PHP,如果PHP是编译安装的,一般就找不到。可以添加一个连接:

ln –sf /usr/local/php/bin/php /usr/local/bin/php

这样这个脚本运行就不会有问题了。然后添加计划任务,让它5分钟运行一次:

crontab –e
0-59/5 * * * * sh /www/magento/public_html/cron.sh > /dev/null 2>&1

不过有时我并不想把这个shell放在根下,我把它挪到比如/root/cron.sh中,这是需要这样调用:

crontab –e
0-59/5 * * * * sh /root/cron.sh /www/magento/public_html/cron.php > /dev/null 2>&1

当放到其它位置时需要指定第二参数,否则找不到执行的PHP脚本。这个shell脚本只是一个包装器,实际上完全可以使用:

crontab –e
0-59/5 * * * * /usr/local/bin/php /www/magento/public_html/cron.php > /dev/null 2>&1

从上面可以看出,cron.php是进入点:

require 'app/Mage.php';
 
if (!Mage::isInstalled()) {
    echo "Application is not installed yet, please complete install wizard first.";
    exit;
}
 
// Only for urls
// Don't remove this
$_SERVER['SCRIPT_NAME'] = str_replace(basename(__FILE__), 'index.php', $_SERVER['SCRIPT_NAME']);
$_SERVER['SCRIPT_FILENAME'] = str_replace(basename(__FILE__), 'index.php', $_SERVER['SCRIPT_FILENAME']);
 
Mage::app('admin')->setUseSessionInUrl(false);
 
umask(0);
 
try {
    Mage::getConfig()->init()->loadEventObservers('crontab');// crontab/events
    Mage::app()->addEventArea('crontab');  //APP  _events['crontab'] = array()
 
    Mage::dispatchEvent('default');
} catch (Exception $e) {
    Mage::printException($e);
}

可以把这里的这个config对象的xml文件输出(注意关闭缓存,否则你会很困惑,因为你可能得到的只是缓存的部分内容,Magento中的对全局配置文件拆分成几个部分缓存)。通过跟踪dispatchEvent函数,里面通过getEventConfig(‘crontab’, ‘default’)获取配置,然后知道实际的配置在Mage/Cron/etc/config.xml中:

<crontab>
    <events>
        <default>
            <observers>
                <cron_observer>
                    <class>cron/observer</class>
                    <method>dispatch</method>
                </cron_observer>
            </observers>
        </default>
    </events>
</crontab>

getEventConfig(‘crontab’, ‘default’)执行实际得到:

object(Mage_Core_Model_Config_Element)#25 (2) { 
    ["default"]=> object(Mage_Core_Model_Config_Element)#72 (1) { 
        ["observers"]=> object(Mage_Core_Model_Config_Element)#73 (1) { 
            ["cron_observer"]=> object(Mage_Core_Model_Config_Element)#16 (2) { 
                ["class"]=> string(13) "cron/observer"
                ["method"]=> string(8) "dispatch"
            }
        } 
    } 
    ["catalog_product_get_final_price"]=> object(Mage_Core_Model_Config_Element)#69 (1) { 
        ["observers"]=> object(Mage_Core_Model_Config_Element)#73 (1) { 
            ["catalogrule"]=> object(Mage_Core_Model_Config_Element)#16 (2) { 
                ["class"]=> string(20) "catalogrule/observer"
                ["method"]=> string(22) "processAdminFinalPrice"
            }
        }
    }
}

我们这里只关注cron/observer的dispatch方法。

public function dispatch($observer)
{
    $schedules = $this->getPendingSchedules();
    //........
 
    $this->generate();
    $this->cleanup();
}

这段代码从cron_schedule中找pending的任务处理,根据时间点判断是否执行。刚开始时,表里并没有任务任务,这个需要靠generate来产生。

public function generate()
 {
     //控制执行周期  systme/cron/schedule_generate_every
     $lastRun = Mage::app()->loadCache(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT);
      
      if ($lastRun > time() - Mage::getStoreConfig(self::XML_PATH_SCHEDULE_GENERATE_EVERY)*60) {
          return $this;
      }
 
     //排除重复创建pending的任务
     $schedules = $this->getPendingSchedules();
     $exists = array();
     foreach ($schedules->getIterator() as $schedule) {
         $exists[$schedule->getJobCode().'/'.$schedule->getScheduledAt()] = 1;
     }
 
     /**
      * generate global crontab jobs
      */
     $config = Mage::getConfig()->getNode('crontab/jobs');
     if ($config instanceof Mage_Core_Model_Config_Element) {
         $this->_generateJobs($config->children(), $exists);
     }
 
     /**
      * generate configurable crontab jobs
      */
     $config = Mage::getConfig()->getNode('default/crontab/jobs');
     if ($config instanceof Mage_Core_Model_Config_Element) {
         $this->_generateJobs($config->children(), $exists);
     }
 
     /**
      * save time schedules generation was ran with no expiration
      */
     Mage::app()->saveCache(time(), self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null);
 
     return $this;
 }

可以看到,它把generate的时间使用缓存文件保存,这样就可以控制generate周期。任务产生配置来自crontab/jobs 和 default/crontab/jobs,输出这两个东西就知道系统提供了计划任务:

Array
(
    [core_clean_cache] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 30 2 * * *
                )
 
            [run] => Array
                (
                    [model] => core/observer::cleanCache
                )
 
        )
 
    [currency_rates_update] => Array
        (
            [run] => Array
                (
                    [model] => directory/observer::scheduledUpdateCurrencyRates
                )
 
        )
 
    [catalog_product_index_price_reindex_all] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 2 * * *
                )
 
            [run] => Array
                (
                    [model] => catalog/observer::reindexProductPrices
                )
 
        )
 
    [catalogrule_apply_all] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 1 * * *
                )
 
            [run] => Array
                (
                    [model] => catalogrule/observer::dailyCatalogUpdate
                )
 
        )
 
    [sales_clean_quotes] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => sales/observer::cleanExpiredQuotes
                )
 
        )
 
    [aggregate_sales_report_order_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => sales/observer::aggregateSalesReportOrderData
                )
 
        )
 
    [aggregate_sales_report_shipment_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => sales/observer::aggregateSalesReportShipmentData
                )
 
        )
 
    [aggregate_sales_report_invoiced_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => sales/observer::aggregateSalesReportInvoicedData
                )
 
        )
 
    [aggregate_sales_report_refunded_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => sales/observer::aggregateSalesReportRefundedData
                )
 
        )
 
    [aggregate_sales_report_bestsellers_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => sales/observer::aggregateSalesReportBestsellersData
                )
 
        )
 
    [aggregate_sales_report_coupons_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => salesrule/observer::aggregateSalesReportCouponsData
                )
 
        )
 
    [system_backup] => Array
        (
            [run] => Array
                (
                    [model] => backup/observer::scheduledBackup
                )
 
        )
 
    [paypal_fetch_settlement_reports] => Array
        (
            [run] => Array
                (
                    [model] => paypal/observer::fetchReports
                )
 
        )
 
    [log_clean] => Array
        (
            [run] => Array
                (
                    [model] => log/cron::logClean
                )
 
        )
 
    [aggregate_sales_report_tax_data] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => tax/observer::aggregateSalesReportTaxData
                )
 
        )
 
    [sitemap_generate] => Array
        (
            [run] => Array
                (
                    [model] => sitemap/observer::scheduledGenerateSitemaps
                )
 
        )
 
    [catalog_product_alert] => Array
        (
            [run] => Array
                (
                    [model] => productalert/observer::process
                )
 
        )
 
    [captcha_delete_old_attempts] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => */30 * * * *
                )
 
            [run] => Array
                (
                    [model] => captcha/observer::deleteOldAttempts
                )
 
        )
 
    [captcha_delete_expired_images] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => */10 * * * *
                )
 
            [run] => Array
                (
                    [model] => captcha/observer::deleteExpiredImages
                )
 
        )
 
    [newsletter_send_all] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => */5 * * * *
                )
 
            [run] => Array
                (
                    [model] => newsletter/observer::scheduledSend
                )
 
        )
 
    [persistent_clear_expired] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => persistent/observer::clearExpiredCronJob
                )
 
        )
 
    [xmlconnect_notification_send_all] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => */5 * * * *
                )
 
            [run] => Array
                (
                    [model] => xmlconnect/observer::scheduledSend
                )
 
        )
 
)
 
 
Array
(
    [catalog_product_alert] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
            [run] => Array
                (
                    [model] => productalert/observer::process
                )
 
        )
 
    [currency_rates_update] => Array
        (
            [schedule] => Array
                (
                    [cron_expr] => 0 0 * * *
                )
 
        )
 
)

系统提供的计划任务还真不少。对应那些提供了run,没有schedule的,一般都是可以后台配置的。

SELECT * FROM core_config_data WHERE path LIKE "%sitemap%"


看到这个图你就清楚,尽管系统配置提供了具体到秒,但是实际上只能具体到分钟。模板中的配置只是一个模板,实际上数据库中的配置会覆盖之前加载的配置。

每个计划任务有两个关键参数schedule和run,如果没有给出schedule,run的模型是不会执行的,同样,没有给出run,那么也没有用。这些配置在每个模块的配置中给出,并且很多模块后台可以配置。cron_expr的参数和Linux的cron语法类似:

//每5分钟执行一次
*/5 * * * *
//或
0-60/5 * * * *
//每两小时执行一次
0 0-24/2 * * *
//前12小时每两小时执行一次
0 0-12/2 * * *
//5 10 15 20分钟执行(具体)
5,10,15,20 0 * * *
//12点和24点 0分执行
0 12,24 * * *

查看源代码可见,如果要每2时执行一次,应该是0-24/2而不是*/2,如果是每2分钟执行一次,则可以是*/2 或 0-60/2,这个是从源代码中得出的结论。

对应计划任务,系统后台提供了一个配置:

这里的几个参数,我是研究了源码才真正搞明白是怎么回事。

History Cleanup Every每隔多少分钟开始清理过期的任务,Success History Lifetime表示成功执行的任务的生存时间,Failure History Lifetime表示执行失败的任务的生存时间(错过执行的任务的生存时间跟这个一样),超过这些生存时间时这些任务将从数据库中删除。

Generate Schedules Every表示创建任务的时间间隔,对于创建了的任务,如果在Missed if Not Run Within指定的时间内没有得到执行,那么就过时(会被清理程序清理)。

Schedule Ahead for表示未来这个时间内,计划有几个任务被执行,比如这里设置了20,有一个计划设置成每5分钟执行一次,那么未来的这20分钟,就有4个任务要执行,分别设置成以5分钟间隔的4个任务写入数据表,这4个任务在时间到时,如果没有超过生存期,则会被执行(具体看系统crontab设置,如果时间比较长,任务可能多个被同时执行)

这个例子是每分钟执行一次,任务创建是在26分钟,但是cron.php再次执行时是在28分钟,所以26,27,28分钟的3个任务都同时执行了。由于Schedule Ahead for是预计未来时间段将要执行的任务,但是产生任务的时间间隔是由Generate Schedules Every决定的,看起来,这个值应该总是比Schedule Ahead for小才是,否则中间将产生空隙。

对于写入数据库里面的pending任务,每次dispatch调用时都被检查是否去执行,这个dispatch多久执行一次就要看系统的crontab设置了。

如果计划没有给它设置表达式,系统根本不会给它创建计划任务。创建了计划任务不一定保证会被执行,可能是在执行的代码中设置了不启用,或者是在计划任务生存时间内没有得到执行,就会错过了时间,那么就不会被执行。如果能够去到执行计划任务,那么这个任务一定会得到成功状态。

在阅读这块源码时,感叹Magento代码的优良与技巧。

发表评论

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