探究 WordPress `wp_cron()` 机制的源码:如何通过页面请求模拟定时任务,并解释其弊端。

大家好,欢迎来到今天的 "WordPress wp_cron() 神秘面纱揭秘" 讲座。我是你们的老朋友,今天咱们就来扒一扒 WordPress 那颗“伪·定时炸弹”—— wp_cron()

先说好,别被名字迷惑了,wp_cron() 并不是一个真正的、独立的定时任务系统。它更像一个“兼职演员”,靠着“碰瓷”用户请求来“假装”定时执行任务。是不是听起来就很滑稽? 别急,咱们慢慢来。

一、wp_cron() 的运行机制:一场“搭便车”的闹剧

wp_cron() 的核心思路很简单:每次有用户访问 WordPress 站点时,它就悄悄地检查一下,有没有到点的定时任务。如果到了,就顺便执行一下。如果没有,那就当没事发生。

这就像你每天早上出门,顺便看看楼下有没有你的快递。有就拿走,没有就直接走人,完全不耽误你上班。

具体流程是这样的:

  1. 用户发起 HTTP 请求: 浏览器输入你的网址,或者点击了某个链接,总而言之,有人访问了你的网站。
  2. WordPress 加载: WordPress 开始解析请求,加载核心文件、主题、插件等等。
  3. wp_cron() 检查: 在 WordPress 加载过程中,wp-includes/cron.php 文件会被加载,这个文件里包含了 wp_cron() 函数。wp_cron() 会检查 wp_options 表中 cron 选项的值,这个值是一个数组,存储了所有定时任务的信息,包括下次运行时间、任务执行的钩子函数等等。
  4. 判断是否到期: wp_cron() 会将当前时间戳与 cron 选项中每个任务的下次运行时间戳进行比较。如果当前时间大于等于某个任务的下次运行时间,就说明这个任务“到期”了,需要执行。
  5. 执行任务: 对于到期的任务,wp_cron() 会触发相应的 WordPress 钩子(action hook)。插件或主题可以通过这些钩子注册自己的函数,从而执行具体的任务。
  6. 更新下次运行时间: 任务执行完毕后,wp_cron() 会根据任务的执行频率(例如,每天、每周等)计算出下次运行的时间戳,并更新 cron 选项中的相应任务信息。
  7. 响应用户请求: 最后,WordPress 完成页面渲染,将结果返回给用户。

二、代码层面:深入 wp-includes/cron.php

光说不练假把式,咱们直接看代码,更直观地了解 wp_cron() 的工作原理。

以下代码简化了 wp-includes/cron.php 中的关键部分,去掉了错误处理和一些细节,只保留了核心逻辑。

<?php

/**
 * Runs scheduled tasks.
 *
 * @param bool $doing_wp_cron Optional. True if doing cron, false otherwise.
 * @return int|void Number of cron events run.
 */
function wp_cron( $doing_wp_cron = false ) {
    global $wpdb;

    if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
        return;
    }

    if ( true === $doing_wp_cron ) {
        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
    }

    if ( is_multisite() ) {
        $pre = $wpdb->base_prefix;
    } else {
        $pre = $wpdb->prefix;
    }

    $crons = _get_cron_array(); // 获取所有的定时任务

    if ( empty( $crons ) ) {
        return;
    }

    $time_now = time();
    $event_count = 0;

    foreach ( $crons as $timestamp => $cronhooks ) {
        if ( $timestamp > $time_now ) {
            break;
        }

        foreach ( $cronhooks as $hook => $args ) {

            if ( ! has_action( $hook ) ) {
                continue;
            }

            spawn_cron(); // 启动一个单独的进程来执行任务,防止阻塞用户请求

            /**
             * Fires just before running a cron job.
             *
             * @since 2.1.0
             *
             * @param string $hook Name of the cron hook to run.
             * @param array  $args Array of cron hook arguments.
             */
            do_action( 'pre_cron_hook', $hook, $args );

            do_action_ref_array( $hook, $args['args'] ); // 执行注册到钩子的函数

            /**
             * Fires immediately after running a cron job.
             *
             * @since 2.1.0
             *
             * @param string $hook Name of the cron hook that was run.
             * @param array  $args Array of cron hook arguments.
             */
            do_action( 'after_cron_hook', $hook, $args );

            $event_count++;
        }
    }
    return $event_count;

}

/**
 * Retrieve cron info array option.
 *
 * @since 2.1.0
 *
 * @return array Cron array.
 */
function _get_cron_array() {
    $crons = get_option( 'cron' );

    if ( ! is_array( $crons ) ) {
        return array();
    }

    foreach ( $crons as $timestamp => $cronhooks ) {
        foreach ( $cronhooks as $hook => $args ) {
            if ( isset( $args['schedule'] ) && ! wp_get_schedule( $args['schedule'] ) ) {
                unset( $crons[ $timestamp ][ $hook ] );
            }
        }
        if ( empty( $crons[ $timestamp ] ) ) {
            unset( $crons[ $timestamp ] );
        }
    }

    return $crons;
}

/**
 * Executes cron.php as a background process.
 *
 * @since 2.1.0
 *
 * @return void
 */
function spawn_cron() {
    if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
        return;
    }

    $cron_url = add_query_arg( 'doing_wp_cron', time(), site_url( 'wp-cron.php' ) );

    // 使用 WordPress 内置的 HTTP API 发起请求
    wp_remote_post( $cron_url, array(
        'timeout'   => 0.01,
        'blocking'  => false,
        'sslverify' => apply_filters( 'https_local_ssl_verify', false )
    ) );
}

代码解读:

  • wp_cron() 函数: 这是 wp_cron() 的核心函数。它首先获取所有已注册的定时任务,然后遍历这些任务,判断是否到期。如果到期,则触发相应的钩子,执行任务。
  • _get_cron_array() 函数: 这个函数负责从 wp_options 表中获取 cron 选项的值,并将其转换为一个数组,方便 wp_cron() 函数使用。
  • spawn_cron() 函数: 这个函数负责启动一个单独的进程来执行定时任务。它通过向 wp-cron.php 发送一个异步 HTTP 请求来实现。 DISABLE_WP_CRON 常量为 true 的时候,这个函数就什么都不做。
  • doing_wp_cron 参数: 这个参数用于区分是用户发起的 HTTP 请求,还是 wp_cron() 自己发起的 HTTP 请求。当 doing_wp_crontrue 时,表示是 wp_cron() 自己发起的请求。

三、注册定时任务:告诉 WordPress 你想做什么

要让 wp_cron() 执行你的任务,你需要先注册一个定时任务。这通常通过 wp_schedule_event() 函数来实现。

<?php
// 注册一个每天执行一次的定时任务
add_action( 'init', 'my_custom_cron_schedule' ); // 在 WordPress 初始化时注册

function my_custom_cron_schedule() {
    if ( ! wp_next_scheduled( 'my_daily_event' ) ) {
        wp_schedule_event( time(), 'daily', 'my_daily_event' );
    }
}

// 定义每天执行的任务
add_action( 'my_daily_event', 'my_daily_task' );

function my_daily_task() {
    // 这里写你的任务代码,例如:
    // 1. 发送邮件
    // 2. 更新数据库
    // 3. 清理缓存
    error_log('每天执行的定时任务');
}

// 添加自定义的计划
add_filter( 'cron_schedules', 'my_custom_cron_schedules' );
function my_custom_cron_schedules( $schedules ) {
    $schedules['every_fifteen_minutes'] = array(
        'interval' => 900, // 900 秒 = 15 分钟
        'display'  => __( '每 15 分钟' )
    );
    return $schedules;
}
?>

代码解读:

  • wp_schedule_event() 函数: 这个函数用于注册一个定时任务。它接受三个参数:
    • $timestamp:任务的首次执行时间戳。
    • $recurrence:任务的执行频率,可以是 hourly(每小时)、daily(每天)、weekly(每周)等 WordPress 内置的频率,也可以是自定义的频率。
    • $hook:任务的钩子名称,当任务到期时,WordPress 会触发这个钩子。
  • add_action() 函数: 这个函数用于将你的任务函数绑定到指定的钩子上。当 WordPress 触发这个钩子时,你的任务函数就会被执行。
  • wp_next_scheduled() 函数: 这个函数检查是否已经有计划的事件。用于避免重复计划同一事件。
  • cron_schedules 过滤器: 用于添加自定义的计划时间。

四、wp_cron() 的弊端:理想很丰满,现实很骨感

虽然 wp_cron() 使用起来很简单,但它也存在一些明显的弊端:

弊端 解释 解决方案
依赖用户请求触发 wp_cron() 的执行完全依赖于用户请求。如果你的网站访问量很低,或者在某个时间段内没有用户访问,那么你的定时任务可能无法按时执行。想象一下,你的网站每天凌晨需要备份数据库,但如果凌晨没有人访问,那么备份任务就会被延迟到下一个用户访问时才执行。 使用真正的 cron 作业; 提高网站访问量(比如做推广); 使用外部服务(例如,使用监控服务定时访问你的网站,触发 wp_cron())。
执行时间不确定 由于 wp_cron() 是在用户请求的处理过程中执行的,因此它的执行时间是不确定的。用户请求的处理时间可能会受到多种因素的影响,例如服务器负载、网络状况、数据库查询等等。这些因素都可能导致 wp_cron() 的执行时间延迟。 同上。
可能会阻塞用户请求 如果你的定时任务需要执行很长时间,那么它可能会阻塞用户请求的处理,导致用户感觉网站响应速度变慢。虽然 spawn_cron() 函数试图将定时任务放在后台执行,但它仍然需要在用户请求的处理过程中启动一个新的进程。如果服务器资源有限,或者定时任务本身非常耗时,那么它仍然可能对用户体验产生负面影响。 尽量缩短定时任务的执行时间; 使用队列系统(例如,RabbitMQ、Beanstalkd)将定时任务放入队列中,然后由独立的进程来处理这些任务。
在高流量网站上可能会导致性能问题 在高流量网站上,wp_cron() 可能会被频繁触发,导致服务器负载过高。每次有用户访问网站时,wp_cron() 都会检查是否有到期的定时任务。如果网站访问量很大,那么 wp_cron() 就会被频繁地调用,从而增加服务器的负担。 禁用 wp_cron(),使用真正的 cron 作业。
依赖 WordPress 环境 wp_cron() 只能在 WordPress 环境中运行。如果你需要在 WordPress 之外执行定时任务,那么 wp_cron() 就无能为力了。 使用真正的 cron 作业,或者使用其他的定时任务系统。
调试困难 由于 wp_cron() 的执行时间不确定,因此调试起来比较困难。你可能需要等待很长时间才能看到定时任务的执行结果。 使用 WordPress 插件来监控和管理 wp_cron(); 启用 WordPress 的调试模式,查看错误日志。

五、替代方案:真正的定时任务系统

既然 wp_cron() 有这么多弊端,那么有没有更好的替代方案呢?当然有!那就是使用真正的定时任务系统,例如:

  • Linux Cron Job: 这是最常用的定时任务系统。你可以在服务器上配置 Cron Job,让它按照指定的时间间隔执行指定的命令或脚本。Linux Cron Job 不依赖于 WordPress,可以在任何环境下运行。
  • Windows 计划任务: 类似于 Linux Cron Job,Windows 计划任务也可以让你按照指定的时间间隔执行指定的程序或脚本。
  • 第三方定时任务服务: 有很多第三方服务提供定时任务功能,例如 EasyCron、Cronitor 等。这些服务通常提供更强大的功能,例如监控、报警等。

六、使用 Linux Cron Job 替代 wp_cron()

咱们以 Linux Cron Job 为例,演示如何替代 wp_cron()

  1. 禁用 wp_cron()wp-config.php 文件中添加以下代码,禁用 wp_cron()

    <?php
    define('DISABLE_WP_CRON', true);
    ?>
  2. 创建 Cron Job 脚本: 创建一个 PHP 脚本,用于执行你的定时任务。例如,创建一个名为 my_cron_script.php 的文件,内容如下:

    <?php
    // 加载 WordPress 环境
    require_once( dirname( __FILE__ ) . '/wp-load.php' );
    
    // 这里写你的任务代码,例如:
    // 1. 发送邮件
    // 2. 更新数据库
    // 3. 清理缓存
    error_log('真正的定时任务');
    ?>
  3. 配置 Cron Job: 使用 crontab -e 命令编辑 Cron Job 配置文件。在文件中添加一行,指定脚本的执行时间和频率。例如,以下代码表示每天凌晨 3 点执行 my_cron_script.php 脚本。

    0 3 * * * /usr/bin/php /path/to/your/wordpress/my_cron_script.php >/dev/null 2>&1

    解释:

    • 0 3 * * *:表示每天凌晨 3 点执行。
    • /usr/bin/php:PHP 解释器的路径。
    • /path/to/your/wordpress/my_cron_script.php:你的 PHP 脚本的路径。
    • >/dev/null 2>&1:将脚本的输出和错误信息都丢弃。

七、总结:wp_cron() 的正确姿势

wp_cron() 就像一个“勤劳的小蜜蜂”,但它需要“花朵”(用户请求)的滋养才能工作。如果你对定时任务的执行时间要求不高,并且你的网站访问量足够大,那么 wp_cron() 还是可以胜任的。

但是,如果你对定时任务的执行时间有严格的要求,或者你的网站访问量很低,那么最好还是使用真正的定时任务系统。

总而言之,wp_cron() 只是一个“备胎”,关键时刻可以顶一下,但不能指望它来承担重任。

今天的讲座就到这里,希望大家对 wp_cron() 有了更深入的了解。记住,选择合适的工具,才能事半功倍! 谢谢大家!

发表回复

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