剖析 WordPress `wp_schedule_single_event()` 函数源码:定时任务在 `wp_options` 中的存储。

各位听众,大家好! 很高兴今天能和大家聊聊 WordPress 里面一个挺重要,但是又容易被忽略的函数 – wp_schedule_single_event()。它负责着我们站点各种定时任务的幕后调度,而这些任务的“日程表”,其实就默默地藏在 wp_options 表里。 咱们今天就来扒一扒它的源码,看看它是怎么把定时任务“塞”进数据库,以及这些数据长什么样。

第一部分:wp_schedule_single_event() 函数概览

首先,让我们先简单了解一下 wp_schedule_single_event() 这个函数是干什么的。 顾名思义,它用于安排一个只执行一次的定时任务。 它的基本语法如下:

wp_schedule_single_event( int $timestamp, string $hook, array $args = array(), string $wp_timezone = '' ): bool
  • $timestamp: 任务执行的时间戳 (Unix timestamp)。 这是个整数,代表从1970年1月1日到指定时间的秒数。
  • $hook: 动作钩子 (action hook) 的名称。 这是个字符串,当定时任务执行时,WordPress 会触发这个钩子。 你可以理解为,当时间到了,WordPress 会“喊一声”这个钩子的名字,然后所有绑定到这个钩子的函数都会被执行。
  • $args: 传递给动作钩子函数的参数数组。 这是一个可选参数,如果你的任务需要一些数据,就可以通过这个数组传递。
  • $wp_timezone: 一个可选参数,如果需要基于其他时区来计划任务,可以使用这个参数。

举个例子,假设我们想在明天早上 8 点执行一个名为 my_custom_task 的任务,并传递一个参数 'hello' => 'world',我们可以这样写:

$tomorrow_8am = strtotime( 'tomorrow 8:00' );
wp_schedule_single_event( $tomorrow_8am, 'my_custom_task', array( 'hello' => 'world' ) );

add_action( 'my_custom_task', 'my_custom_task_callback', 10, 1 );

function my_custom_task_callback( $args ) {
  // Do something with $args
  error_log( "Task executed with args: " . print_r( $args, true ) );
}

这段代码的意思是:在明天早上 8 点,触发 my_custom_task 这个动作钩子。 并且,my_custom_task_callback 函数会接收到 array( 'hello' => 'world' ) 这个参数。 error_log 会将参数输出到错误日志中,方便我们查看。

第二部分:深入 wp_schedule_single_event() 源码

现在,让我们深入到 wp-includes/cron.php 文件中,看看 wp_schedule_single_event() 函数的真面目。 (这里简化了部分代码,只保留了核心逻辑)

function wp_schedule_single_event( int $timestamp, string $hook, array $args = array(), string $wp_timezone = '' ): bool {
    $crons = _get_cron_array(); // 获取当前的 cron 任务数组

    if ( isset( $crons[ $timestamp ][ $hook ] ) ) {
        foreach ( (array) $crons[ $timestamp ][ $hook ] as $key => $value ) {
            if ( is_array( $args ) && is_array( $value['args'] ) && $args === $value['args'] ) {
                return false; // 已经存在相同的任务,直接返回
            }
        }
    }

    $crons[ $timestamp ][ $hook ][ uniqid() ] = array(
        'schedule' => false, // 单次事件的 schedule 值为 false
        'args'     => $args,
    );

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

    return _set_cron_array( $crons ); // 更新 cron 任务数组到数据库
}

这段代码主要做了以下几件事:

  1. 获取当前的 Cron 任务数组: $crons = _get_cron_array(); 这行代码通过 _get_cron_array() 函数从数据库中获取当前所有的定时任务。 _get_cron_array 函数会从 wp_options 表中读取名为 cron 的 option,这个 option 存储着一个序列化的数组,包含了所有的定时任务信息。
  2. 检查是否已存在相同的任务: 代码会遍历 $crons 数组,检查是否已经存在相同时间戳、相同钩子和相同参数的任务。 如果存在,则直接返回 false,避免重复添加。
  3. 添加新的任务到数组: 如果不存在相同的任务,则将新的任务添加到 $crons 数组中。 注意,这里使用 uniqid() 函数生成一个唯一的 key,以防止不同的任务使用相同的时间戳和钩子时发生冲突。 schedule 字段被设置为 false,表示这是一个单次事件。
  4. 按时间戳排序: uksort( $crons, 'strnatcmp' ); 这行代码使用 uksort 函数对 $crons 数组按照时间戳进行排序。 这样做是为了保证任务按照时间顺序执行。 strnatcmp 提供了一种更自然的字符串比较方式,尤其是在处理包含数字的字符串时。
  5. 更新 Cron 任务数组到数据库: return _set_cron_array( $crons ); 这行代码通过 _set_cron_array() 函数将更新后的 $crons 数组保存到数据库中。 _set_cron_array 函数会将数组序列化,并更新 wp_options 表中名为 cron 的 option。

第三部分:_get_cron_array()_set_cron_array() 函数

既然 wp_schedule_single_event() 依赖于 _get_cron_array()_set_cron_array() 这两个函数,那我们再来看看它们是怎么工作的。

  • _get_cron_array() 函数:
function _get_cron_array() {
    global $wp_filter;

    $crons = get_option( 'cron' );

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

    /**
     * Filters the cron array.
     *
     * @since 3.0.0
     *
     * @param array $crons An array of cron events.
     */
    return apply_filters( 'pre_get_cron_array', $crons );
}

这个函数很简单,就是从 wp_options 表中读取名为 cron 的 option,并返回它的值。 如果这个 option 不存在,或者不是一个数组,则返回一个空数组。 这里使用了 apply_filters 钩子,允许其他插件或主题修改 cron 数组。

  • _set_cron_array() 函数:
function _set_cron_array( $crons ) {
    /**
     * Filters the cron array before it is updated.
     *
     * @since 3.0.0
     *
     * @param array $crons An array of cron events.
     */
    $crons = apply_filters( 'pre_set_cron_array', $crons );

    if ( empty( $crons ) ) {
        return delete_option( 'cron' );
    }

    return update_option( 'cron', $crons );
}

这个函数也很简单,就是将传入的 $crons 数组保存到 wp_options 表中,作为名为 cron 的 option 的值。 如果 $crons 数组为空,则删除这个 option。 这里同样使用了 apply_filters 钩子,允许其他插件或主题在 cron 数组保存之前进行修改。

第四部分:定时任务在 wp_options 表中的存储形式

现在,我们终于可以揭开定时任务在 wp_options 表中的神秘面纱了。 打开你的 WordPress 数据库,找到 wp_options 表(表前缀可能不同,比如 xyz_options)。 然后,搜索 option_namecron 的记录。 你会发现 option_value 字段存储着一个很长的字符串,这个字符串其实就是一个序列化的 PHP 数组

为了更好地理解这个数组的结构,我们可以使用 unserialize() 函数将这个字符串反序列化。 假设我们从数据库中读取到的 option_value 如下:

a:2:{i:1678886400;a:1:{s:15:"my_custom_task";a:1:{s:13:"641e36648b9a3";a:2:{s:8:"schedule";b:0;s:4:"args";a:1:{s:5:"hello";s:5:"world";}}}}i:1678890000;a:1:{s:15:"another_task";a:1:{s:13:"641e36648b9b4";a:2:{s:8:"schedule";b:0;s:4:"args";a:0:{}}}}}

将它反序列化后,我们会得到一个类似这样的数组:

array(
  1678886400 => array(
    'my_custom_task' => array(
      '641e36648b9a3' => array(
        'schedule' => false,
        'args' => array(
          'hello' => 'world',
        ),
      ),
    ),
  ),
  1678890000 => array(
    'another_task' => array(
      '641e36648b9b4' => array(
        'schedule' => false,
        'args' => array(),
      ),
    ),
  ),
)

这个数组的结构可以总结如下:

  • 第一层: 键是时间戳 (Unix timestamp),值是一个数组,包含了在这个时间点需要执行的所有任务。
  • 第二层: 键是动作钩子 (action hook) 的名称,值是一个数组,包含了绑定到这个钩子的所有任务。
  • 第三层: 键是一个唯一的 ID (通过 uniqid() 生成),值是一个数组,包含了任务的详细信息,例如 scheduleargs
    • schedule 字段表示任务的执行频率。 对于单次事件,它的值是 false。 对于循环事件,它的值是循环的间隔,例如 'hourly''daily''weekly'
    • args 字段是一个数组,包含了传递给动作钩子函数的参数。

为了更清晰地展示这个结构,我们可以使用表格:

层级 描述
1 时间戳 (timestamp) 数组 (array) 表示任务执行的时间点。
2 动作钩子 (hook) 数组 (array) 表示需要触发的动作钩子的名称。
3 唯一 ID (uniqid) 数组 (array) 表示一个具体的任务实例。
4 schedule 布尔值 (boolean) 或 字符串 (string) 对于单次事件,值为 false。对于循环事件,值为循环间隔,例如 'hourly''daily''weekly'
4 args 数组 (array) 传递给动作钩子函数的参数。

第五部分:Cron 任务的执行机制

现在我们知道了定时任务是如何存储在数据库中的,那么 WordPress 是如何执行这些任务的呢?

WordPress 依赖于一个名为 WP-Cron 的系统来执行定时任务。 WP-Cron 并不是一个真正的系统级别的 cron 守护进程,而是一个模拟的 cron 系统。 它的工作方式如下:

  1. 每次有用户访问你的 WordPress 站点时,WordPress 都会检查是否有需要执行的定时任务。 具体来说,WordPress 会比较当前时间戳和 wp_options 表中 cron option 存储的每个任务的时间戳。
  2. 如果找到了需要执行的任务,WordPress 就会触发相应的动作钩子,并执行绑定到这些钩子的函数。
  3. 执行完任务后,WordPress 会更新 wp_options 表中的 cron option,移除已经执行的任务。 对于循环任务,WordPress 会根据任务的循环间隔,计算出下一次执行的时间戳,并更新 cron option。

WP-Cron 的优点是简单易用,无需配置服务器级别的 cron 守护进程。 但是,它的缺点是依赖于用户的访问才能触发任务。 如果你的站点访问量很低,那么定时任务可能无法按时执行。

为了解决这个问题,你可以配置一个真正的系统级别的 cron 守护进程,定期访问 wp-cron.php 文件。 这样可以保证定时任务即使在站点没有用户访问的情况下也能按时执行。

第六部分:总结与建议

通过今天的分析,我们了解了 wp_schedule_single_event() 函数是如何将定时任务存储在 wp_options 表中的,以及 WordPress 是如何通过 WP-Cron 系统执行这些任务的。

以下是一些建议:

  • 尽量不要安排过多的定时任务。 过多的定时任务会增加数据库的负担,并可能影响站点的性能。
  • 合理设置定时任务的执行时间。 避免在高峰时段执行占用大量资源的定时任务。
  • 考虑使用系统级别的 cron 守护进程。 如果你的站点访问量很低,或者需要保证定时任务的准时执行,可以考虑使用系统级别的 cron 守护进程。
  • 注意时区问题。 确保你的 WordPress 站点和服务器的时区设置正确,以避免定时任务执行时间出现偏差。
  • 使用插件来管理定时任务。 有一些插件可以帮助你更方便地管理和调试定时任务,例如 WP Crontrol。

希望今天的讲解对大家有所帮助。 谢谢大家!

发表回复

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