解释 WordPress 的 `wp_schedule_single_event()` 和 `wp_schedule_event()` 函数的源码,它们是如何实现定时任务的?

大家好,欢迎来到今天的“WordPress定时炸弹”讲座!别害怕,这里的“炸弹”不是真炸弹,而是我们今天的主角——WordPress 的定时任务。 今天我们要深入探讨 wp_schedule_single_event()wp_schedule_event() 这两个“定时炸弹”的源码和工作原理,保证让你听完之后,也能成为 WordPress 定时任务的高手。

一、为什么我们需要定时任务?

想象一下,你有个博客,每天想定时发布一篇新的文章,或者每天凌晨定时清理一下数据库垃圾数据。如果每次都要手动操作,那得多累啊!这时候,定时任务就派上用场了。WordPress 的定时任务允许我们在指定的时间执行特定的代码,解放我们的双手,让 WordPress 自动完成一些重复性的工作。

二、WordPress 的定时任务机制:WP-Cron

在深入 wp_schedule_single_event()wp_schedule_event() 之前,我们需要了解 WordPress 定时任务的大管家——WP-Cron。

WP-Cron 并不是一个真正的系统级别的 cron 任务。它更像是一个“伪 cron”,或者说是一个“计划任务模拟器”。 它的工作方式是:当有用户访问你的 WordPress 网站时,WP-Cron 会检查是否有需要执行的定时任务。如果有,它就会执行这些任务。

所以,如果你的网站访问量很低,WP-Cron 可能就无法按时执行任务。这就是为什么有些时候,你的定时任务会延迟执行的原因。

三、wp_schedule_single_event():一次性定时炸弹

wp_schedule_single_event() 函数用于安排一个 只执行一次 的定时任务。 它的用法很简单:

wp_schedule_single_event( $timestamp, $hook, $args );
  • $timestamp:任务执行的时间戳(Unix 时间戳)。
  • $hook:与该任务关联的钩子(action hook)。当任务执行时,WordPress 会触发这个钩子。
  • $args:传递给钩子函数的参数(数组)。

现在,让我们深入源码,看看 wp_schedule_single_event() 到底做了什么:

function wp_schedule_single_event( $timestamp, $hook, $args = array() ) {
    $crons = _get_cron_array(); // 获取当前已计划的所有任务
    $key = md5( $hook . serialize( $args ) ); // 生成任务的唯一标识符

    if ( isset( $crons[ $timestamp ][ $key ] ) ) {
        return false; // 如果该任务已经存在,则直接返回 false
    }

    $crons[ $timestamp ][ $key ] = array(
        'hook' => $hook,
        'args' => $args,
        'schedule' => false // 标记为单次事件
    );

    uksort( $crons, 'strnatcmp' ); // 按照时间戳排序

    return _set_cron_array( $crons ); // 保存更新后的任务列表
}
  1. _get_cron_array():获取任务列表

    这个函数负责从 WordPress 的 options 表中获取已计划的所有任务。任务数据存储在 cron 选项中,以数组的形式存储。

    function _get_cron_array() {
        $crons = get_option( 'cron' );
    
        if ( ! is_array( $crons ) ) {
            return array();
        }
    
        foreach ( $crons as $timestamp => $cronhooks ) {
            foreach ( (array) $cronhooks as $hook => $keys ) {
                if ( preg_match( '/^[A-Za-z0-9_-]+$/', $hook ) === 0 ) {
                    unset( $crons[ $timestamp ][ $hook ] );
                }
            }
    
            if ( empty( $crons[ $timestamp ] ) ) {
                unset( $crons[ $timestamp ] );
            }
        }
    
        return $crons;
    }
    • get_option( 'cron' ):从数据库中获取 cron 选项的值。
    • 代码检查任务的 hook 是否有效,无效的则删除。
    • 如果某个时间戳下没有任何任务,则删除该时间戳。
  2. md5( $hook . serialize( $args ) ):生成唯一标识符

    为了防止重复添加相同的任务,wp_schedule_single_event() 会根据 $hook$args 生成一个 MD5 散列值作为任务的唯一标识符。这样,即使你多次调用 wp_schedule_single_event() 并且参数相同,也只会添加一个任务。

  3. $crons[ $timestamp ][ $key ] = ...:添加任务到数组

    将新的任务添加到 $crons 数组中。 数组的结构是这样的:

    $crons = array(
        $timestamp => array( // 时间戳
            $key => array( // 任务的唯一标识符
                'hook' => $hook, // 钩子名称
                'args' => $args, // 参数
                'schedule' => false // 标记为单次事件
            )
        )
    );
  4. uksort( $crons, 'strnatcmp' ):按时间戳排序

    uksort() 函数使用 strnatcmp() 函数对 $crons 数组的键(时间戳)进行排序。strnatcmp() 函数执行的是“自然排序”,可以正确地处理包含数字的时间戳。

  5. _set_cron_array( $crons ):保存任务列表

    这个函数负责将更新后的任务列表保存到 WordPress 的 options 表中。

    function _set_cron_array( $crons ) {
        return update_option( 'cron', $crons );
    }
    • update_option( 'cron', $crons ):将 $crons 数组保存到数据库的 cron 选项中。

四、wp_schedule_event():周期性定时炸弹

wp_schedule_event() 函数用于安排一个 周期性 的定时任务。 它的用法如下:

wp_schedule_event( $timestamp, $recurrence, $hook, $args );
  • $timestamp:第一次执行任务的时间戳(Unix 时间戳)。
  • $recurrence:任务的执行周期(字符串)。可以是 WordPress 预定义的周期,也可以是自定义的周期。
  • $hook:与该任务关联的钩子(action hook)。当任务执行时,WordPress 会触发这个钩子。
  • $args:传递给钩子函数的参数(数组)。

WordPress 预定义的周期包括:

周期 描述
hourly 每小时一次
twicedaily 每天两次
daily 每天一次
weekly 每周一次

同样,让我们深入源码,看看 wp_schedule_event() 做了什么:

function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) {
    $crons = _get_cron_array(); // 获取当前已计划的所有任务
    $key = md5( $hook . serialize( $args ) . $recurrence ); // 生成任务的唯一标识符

    if ( isset( $crons[ $timestamp ][ $key ] ) ) {
        return false; // 如果该任务已经存在,则直接返回 false
    }

    $crons[ $timestamp ][ $key ] = array(
        'hook' => $hook,
        'args' => $args,
        'schedule' => $recurrence // 存储任务的执行周期
    );

    uksort( $crons, 'strnatcmp' ); // 按照时间戳排序

    return _set_cron_array( $crons ); // 保存更新后的任务列表
}

wp_schedule_event() 的代码与 wp_schedule_single_event() 非常相似,主要区别在于以下两点:

  1. 唯一标识符的生成

    wp_schedule_event() 中,唯一标识符的生成包含了 $recurrence 参数,这样可以区分相同 hook 和 args 的不同周期的事件。

    $key = md5( $hook . serialize( $args ) . $recurrence );
  2. 'schedule' => $recurrence

    wp_schedule_event() 中,'schedule' 键的值是 $recurrence,表示任务的执行周期。

五、WP-Cron 如何执行任务?

当有用户访问你的 WordPress 网站时,wp-cron.php 会被触发(实际上是通过 HTTP 请求触发)。 wp-cron.php 文件会检查是否有需要执行的定时任务。

wp-cron.php 文件的核心代码如下:

if ( ! defined( 'DOING_CRON' ) ) {
    define( 'DOING_CRON', true );
}

// 加载 WordPress 核心文件
require_once( dirname( __FILE__ ) . '/wp-load.php' );

// 检查是否禁用 WP-Cron
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
    return;
}

// 检查是否正在运行其他 WP-Cron 进程
if ( wp_doing_cron() ) {
    return;
}

// 设置超时时间
if ( ! defined( 'WP_CRON_LOCK_TIMEOUT' ) ) {
    define( 'WP_CRON_LOCK_TIMEOUT', 60 ); // 默认 60 秒
}

// 获取当前时间
$time_now = time();

// 获取需要执行的任务列表
$crons = _get_cron_array();

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

// 遍历任务列表,执行到期的任务
foreach ( $crons as $timestamp => $cronhooks ) {
    if ( $timestamp > $time_now ) {
        break; // 任务还未到期,退出循环
    }

    foreach ( (array) $cronhooks as $hook => $keys ) {
        foreach ( (array) $keys as $key => $data ) {
            if ( ! has_action( $data['hook'] ) ) {
                continue; // 如果钩子不存在,则跳过
            }

            do_action_ref_array( $data['hook'], $data['args'] ); // 执行钩子函数

            // 如果是周期性任务,则重新计划任务
            if ( $data['schedule'] ) {
                $new_timestamp = wp_next_scheduled( $hook, $data['args'] );
                if ( $new_timestamp ) {
                    wp_unschedule_event( $timestamp, $hook, $data['args'] );
                    wp_schedule_event( $new_timestamp, $data['schedule'], $hook, $data['args'] );
                }
            } else {
                // 如果是单次任务,则取消计划
                wp_unschedule_event( $timestamp, $hook, $data['args'] );
            }
        }
    }
}
  1. _get_cron_array():获取任务列表

    wp_schedule_single_event()wp_schedule_event() 中使用的 _get_cron_array() 函数相同,用于从数据库中获取任务列表。

  2. 遍历任务列表,执行到期的任务

    wp-cron.php 遍历任务列表,如果任务的执行时间戳小于当前时间戳,则执行该任务。

  3. do_action_ref_array( $data['hook'], $data['args'] ):执行钩子函数

    do_action_ref_array() 函数用于触发与任务关联的钩子函数。$data['hook'] 是钩子的名称,$data['args'] 是传递给钩子函数的参数。

  4. 重新计划周期性任务

    如果任务是周期性的,wp-cron.php 会使用 wp_schedule_event() 函数重新计划该任务,以便在下一个周期继续执行。

  5. 取消计划单次任务

    如果任务是单次的,wp-cron.php 会使用 wp_unschedule_event() 函数取消计划该任务,以防止重复执行。

六、wp_unschedule_event():取消定时炸弹

wp_unschedule_event() 函数用于取消已计划的定时任务。 它的用法如下:

wp_unschedule_event( $timestamp, $hook, $args );
  • $timestamp:任务执行的时间戳(Unix 时间戳)。
  • $hook:与该任务关联的钩子(action hook)。
  • $args:传递给钩子函数的参数(数组)。

让我们看看 wp_unschedule_event() 的源码:

function wp_unschedule_event( $timestamp, $hook, $args = array() ) {
    $crons = _get_cron_array(); // 获取当前已计划的所有任务
    $key = md5( $hook . serialize( $args ) ); // 生成任务的唯一标识符

    if ( ! isset( $crons[ $timestamp ][ $key ] ) ) {
        return false; // 如果该任务不存在,则直接返回 false
    }

    unset( $crons[ $timestamp ][ $key ] ); // 从任务列表中移除该任务

    if ( empty( $crons[ $timestamp ] ) ) {
        unset( $crons[ $timestamp ] ); // 如果该时间戳下没有任何任务,则移除该时间戳
    }

    return _set_cron_array( $crons ); // 保存更新后的任务列表
}

wp_unschedule_event() 函数首先获取任务列表,然后根据 $timestamp$hook$args 生成唯一标识符,找到要取消的任务,并将其从任务列表中移除。最后,将更新后的任务列表保存到数据库中。

七、自定义定时任务周期

WordPress 允许我们自定义定时任务的周期。我们可以使用 cron_schedules 过滤器来添加自定义的周期。

add_filter( 'cron_schedules', 'my_custom_cron_schedule' );

function my_custom_cron_schedule( $schedules ) {
    $schedules['every_five_minutes'] = array(
        'interval' => 300, // 5 分钟,单位为秒
        'display'  => __( 'Every 5 Minutes' )
    );

    return $schedules;
}
  • 'every_five_minutes':自定义周期的名称。
  • 'interval' => 300:周期的间隔时间,单位为秒。
  • 'display' => __( 'Every 5 Minutes' ):周期在 WordPress 后台显示的名称。

添加了自定义的周期后,我们就可以在 wp_schedule_event() 函数中使用它了:

wp_schedule_event( time(), 'every_five_minutes', 'my_custom_hook' );

八、一些注意事项

  • WP-Cron 的可靠性问题: 由于 WP-Cron 依赖于用户访问来触发,因此如果你的网站访问量很低,定时任务可能会延迟执行。为了解决这个问题,你可以使用系统级别的 cron 任务来定期访问 wp-cron.php 文件。
  • 任务的唯一性: 在添加定时任务时,要确保任务的 $hook$args 参数能够唯一标识该任务,避免重复添加相同的任务。
  • 任务的执行时间: 要合理设置任务的执行时间,避免在网站访问高峰期执行耗时较长的任务,影响用户体验。
  • 错误处理: 在编写定时任务的钩子函数时,要做好错误处理,避免因为一个任务的错误导致整个 WP-Cron 进程崩溃。可以使用 try...catch 语句来捕获异常,并记录错误日志。
  • 调试定时任务: 可以使用插件,或者修改 wp-config.php 文件,增加 define('WP_DEBUG', true);define('WP_DEBUG_LOG', true); 来开启调试模式,查看错误日志。

九、总结

wp_schedule_single_event()wp_schedule_event() 是 WordPress 定时任务的核心函数。它们允许我们安排单次和周期性的任务,让 WordPress 自动完成一些重复性的工作。

通过深入了解这两个函数的源码和 WP-Cron 的工作原理,我们可以更好地利用 WordPress 的定时任务机制,提高我们的开发效率。

今天的讲座就到这里。希望大家对 WordPress 的定时任务有了更深入的了解。 记住,定时任务虽然强大,但也要谨慎使用,避免滥用导致性能问题。 感谢大家的参与!

发表回复

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