深入理解 WordPress `get_option()` 函数的源码:它如何利用对象缓存来避免重复数据库查询。

各位技术控,大家好!我是今天的主讲人,咱们今天来聊聊WordPress里一个看似简单,实则暗藏玄机的函数:get_option()

这玩意儿,几乎每个WordPress插件和主题都在用,但你真的了解它背后的原理吗?特别是它如何利用对象缓存来提升性能,避免动不动就去数据库里“翻箱倒柜”?今天咱们就来扒一扒它的源码,彻底搞懂它!

一、get_option():你以为的只是个“取值器”?

get_option() 的作用很简单,就是根据你提供的 option name,从数据库的 wp_options 表中读取对应的 option value。 比如:

$site_title = get_option('blogname');
echo "我的网站名字是:" . $site_title;

这段代码会获取你网站的标题,并显示出来。但是,如果仅仅是这样,那 get_option() 就太普通了,远不值得我们专门花时间来研究。

二、wp_options 表:WordPress 的“小金库”

想要深入了解 get_option(),首先得了解一下 wp_options 表。这个表是WordPress用来存储各种配置选项的地方,比如网站标题、主题设置、插件配置等等。

wp_options 表的结构大致如下:

字段名 数据类型 描述
option_id BIGINT 自增ID,主键
option_name VARCHAR(191) 选项名称,用于查找对应的选项值
option_value LONGTEXT 选项值,存储实际的配置数据
autoload VARCHAR(20) 是否自动加载,取值为 yesno

autoload 字段非常重要,它决定了WordPress在初始化时,是否将该选项加载到内存中。 如果autoloadyes,那么这个option会在WordPress启动时被加载到缓存中,以便快速访问。

三、get_option() 的源码剖析:缓存才是重点!

好了,有了上面的基础,咱们终于可以开始看 get_option() 的源码了。为了方便理解,我们只关注核心部分,忽略一些错误处理和兼容性代码。

function get_option( $option, $default = false ) {
    global $wpdb;

    static $notoptions; // 静态变量,用于缓存不存在的 option

    if ( ! isset( $notoptions ) ) {
        $notoptions = wp_cache_get( 'notoptions', 'options' );
        if ( ! is_array( $notoptions ) ) {
            $notoptions = array();
        }
    }

    // 1. 检查是否在 $notoptions 缓存中
    if ( isset( $notoptions[ $option ] ) ) {
        return $default;
    }

    // 2. 尝试从对象缓存中获取
    $value = wp_cache_get( $option, 'options' );

    if ( false !== $value ) {
        return $value;
    }

    // 3. 如果缓存中没有,则从数据库中读取
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );

    // 4. 处理从数据库中读取到的数据
    if ( is_object( $row ) ) {
        $value = $row->option_value;

        // Maybe unserialize it if necessary.
        if ( 'yes' == get_option( 'maybe_serialize' ) ) {
            $value = maybe_unserialize( $value );
        }

        wp_cache_set( $option, $value, 'options' ); // 存入对象缓存

        return $value;
    }

    // 5. 如果数据库中也没有,则存入 $notoptions 缓存,并返回默认值
    $notoptions[ $option ] = true;
    wp_cache_set( 'notoptions', $notoptions, 'options' );

    return $default;
}

代码解读:

  1. $notoptions 缓存: 这是一个静态变量,用于缓存那些不存在的 option。 如果某个 option 之前已经被查询过,但数据库中不存在,那么它会被记录在 $notoptions 中。 这样,下次再查询这个 option 时,get_option() 就可以直接返回默认值,而不需要再次访问数据库。 这是一种非常巧妙的优化,避免了对不存在的 option 进行重复查询。

  2. 对象缓存: wp_cache_get( $option, 'options' )get_option() 的核心所在。 它尝试从对象缓存中获取指定 option 的值。 这里的 'options' 是一个缓存组(group),用于区分不同类型的缓存数据。 如果缓存命中,则直接返回缓存中的值,大大提高了性能。

  3. 数据库查询: 如果对象缓存中没有找到对应的 option,get_option() 才会去数据库中查询。 使用了 $wpdb->prepare() 来防止 SQL 注入。

  4. 数据处理: 从数据库中读取到的 option_value 可能是序列化后的字符串,需要使用 maybe_unserialize() 进行反序列化。 然后,将读取到的值存入对象缓存,以便下次快速访问。

  5. 不存在的 Option: 如果数据库中也没有找到对应的 option,那么将该 option 记录到 $notoptions 缓存中,并返回默认值。

四、对象缓存:WordPress 的“记忆力”

get_option() 之所以能如此高效,很大程度上得益于对象缓存机制。 对象缓存可以将数据存储在内存中,以便快速访问,避免重复的数据库查询。

WordPress 支持多种对象缓存后端,比如:

  • Transient API: 使用数据库存储缓存数据,但设置了过期时间。
  • Memcached: 一个高性能的分布式内存对象缓存系统。
  • Redis: 一个高级的键值存储数据库,可以用作对象缓存。

默认情况下,WordPress 使用 Transient API 作为对象缓存的后端。 但是,为了获得更好的性能,建议使用 Memcached 或 Redis。

五、wp_cache_get()wp_cache_set():缓存的“搬运工”

wp_cache_get()wp_cache_set() 是 WordPress 提供的用于操作对象缓存的函数。

  • wp_cache_get( $key, $group, $force = false, &$found = null ): 从对象缓存中获取指定 key 的值。

    • $key: 缓存的键名。
    • $group: 缓存组,用于区分不同类型的缓存数据。
    • $force: 是否强制从数据库中读取数据,忽略缓存。
    • $found: 一个引用参数,用于指示是否找到了缓存数据。
  • wp_cache_set( $key, $data, $group, $expire = 0 ): 将数据存储到对象缓存中。

    • $key: 缓存的键名。
    • $data: 要缓存的数据。
    • $group: 缓存组。
    • $expire: 缓存过期时间,单位为秒。 如果为 0,则表示永不过期。

六、update_option()delete_option():option 的“管理者”

除了 get_option(),WordPress 还提供了 update_option()delete_option() 函数,用于更新和删除 option。 这两个函数也会更新对象缓存,以保持缓存数据与数据库数据的一致性。

  • update_option( $option, $value, $autoload = null ): 更新 option 的值。
    1. 它首先会检查对象缓存中是否存在该 option。
    2. 如果存在,则更新缓存中的值。
    3. 然后,它会将新的值写入数据库。
function update_option( $option, $value, $autoload = null ) {
    global $wpdb;

    if ( is_protected_option( $option ) ) {
        return false;
    }

    $option = trim( $option );
    if ( empty( $option ) ) {
        return false;
    }

    $old_value = get_option( $option );

    $value = sanitize_option( $option, $value );

    /**
     * Fires before an option is updated.
     *
     * @since 2.0.0
     *
     * @param string $option  Name of the option to update.
     * @param mixed  $old_value The old option value.
     * @param mixed  $value     The new option value.
     */
    do_action( 'update_option', $option, $old_value, $value );

    // If the new and old values are the same, no need to update.
    if ( maybe_serialize( $old_value ) === maybe_serialize( $value ) ) {
        return false;
    }

    /**
     * Filters a specific option value before it is updated.
     *
     * @since 2.3.0
     *
     * @param mixed  $value     The new, unserialized option value.
     * @param string $option  Name of the option.
     * @param mixed  $old_value The old option value.
     */
    $value = apply_filters( "pre_update_option_{$option}", $value, $option, $old_value );

    /**
     * Filters an option value before it is updated.
     *
     * @since 2.3.0
     *
     * @param mixed  $value     The new, unserialized option value.
     * @param string $option  Name of the option.
     * @param mixed  $old_value The old option value.
     */
    $value = apply_filters( 'pre_update_option', $value, $option, $old_value );

    $serialized_value = maybe_serialize( $value );

    $data  = array( 'option_value' => $serialized_value );
    $where = array( 'option_name' => $option );
    $format = array( '%s' );
    $where_format = array( '%s' );

    if ( is_null( $autoload ) ) {
        $autoload = 'yes';
    }

    if ( 'yes' !== $autoload && 'no' !== $autoload ) {
        return false;
    }

    if ( get_option( 'maybe_serialize' ) == 'yes' ) {
        $autoload = 'yes';
    }

    $option_exists = $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM $wpdb->options WHERE option_name = %s", $option ) );

    if ( $option_exists ) {
        /**
         * Fires immediately before an existing option is updated.
         *
         * @since 2.9.0
         *
         * @param string $option  Name of the option to update.
         * @param mixed  $old_value The old option value.
         * @param mixed  $value     The new option value.
         */
        do_action( 'updating_option', $option, $old_value, $value );

        $result = $wpdb->update( $wpdb->options, $data, $where, $format, $where_format );

        if ( ! $result ) {
            return false;
        }

        wp_cache_delete( $option, 'options' ); // 删除缓存
        wp_cache_set( $option, $value, 'options' ); // 重新设置缓存

        /**
         * Fires immediately after an existing option is updated.
         *
         * @since 2.0.0
         *
         * @param string $option  Name of the option updated.
         * @param mixed  $old_value The old option value.
         * @param mixed  $value     The new option value.
         */
        do_action( 'updated_option', $option, $old_value, $value );

    } else {
        /**
         * Fires immediately before a new option is added.
         *
         * @since 2.9.0
         *
         * @param string $option Name of the option to add.
         * @param mixed  $value  The option value.
         */
        do_action( 'add_option', $option, $value );

        $result = $wpdb->insert( $wpdb->options,
            array(
                'option_name'  => $option,
                'option_value' => $serialized_value,
                'autoload'     => $autoload,
            ),
            array(
                '%s',
                '%s',
                '%s',
            )
        );
        if ( ! $result ) {
            return false;
        }

        $option_id = $wpdb->insert_id;

        wp_cache_add( $option, $value, 'options' ); // 添加缓存

        /**
         * Fires immediately after a new option has been added.
         *
         * @since 2.0.0
         *
         * @param string $option Name of the option added.
         * @param mixed  $value  The option value.
         */
        do_action( 'added_option', $option, $value );
    }

    return true;
}
  • delete_option( $option ): 删除 option。
    1. 它首先会从数据库中删除该 option。
    2. 然后,它会从对象缓存中删除对应的缓存数据。
function delete_option( $option ) {
    global $wpdb;

    if ( is_protected_option( $option ) ) {
        return false;
    }

    $option = trim( $option );
    if ( empty( $option ) ) {
        return false;
    }

    /**
     * Fires immediately before an option is deleted.
     *
     * @since 2.9.0
     *
     * @param string $option Name of the option to delete.
     */
    do_action( 'delete_option', $option );

    $result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option ), array( '%s' ) );
    if ( ! $result ) {
        return false;
    }

    wp_cache_delete( $option, 'options' ); // 删除缓存
    $notoptions = wp_cache_get( 'notoptions', 'options' );
    if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
        unset( $notoptions[ $option ] );
        wp_cache_set( 'notoptions', $notoptions, 'options' );
    }

    /**
     * Fires immediately after an option is deleted.
     *
     * @since 2.0.0
     *
     * @param string $option Name of the option deleted.
     */
    do_action( 'deleted_option', $option );

    return true;
}

七、autoload 字段:启动时的“预加载”

前面提到了 wp_options 表中的 autoload 字段。 如果一个 option 的 autoload 值为 yes,那么 WordPress 在启动时,会将该 option 加载到对象缓存中。 这样,在后续的请求中,get_option() 就可以直接从缓存中获取值,而不需要访问数据库。

WordPress 提供了一个函数 wp_load_alloptions() 用于加载所有 autoloadyes 的 option。

function wp_load_alloptions() {
    global $wpdb;

    if ( ! wp_installing() || ! is_multisite() ) {
        $alloptions = wp_cache_get( 'alloptions', 'options' );
        if ( ! $alloptions ) {
            $alloptions = array();
            $options = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" );
            foreach ( (array) $options as $option ) {
                $alloptions[ $option->option_name ] = maybe_unserialize( $option->option_value );
            }
            wp_cache_add( 'alloptions', $alloptions, 'options' );
        }
    } else {
        $alloptions = array();
    }

    return $alloptions;
}

八、优化建议:让 get_option() 飞起来!

  1. 选择合适的缓存后端: 使用 Memcached 或 Redis 代替默认的 Transient API,可以显著提高性能。
  2. 合理设置 autoload 字段: 只将常用的 option 设置为 autoload = yes,避免加载过多的数据到内存中。
  3. 避免频繁更新 option: 频繁更新 option 会导致缓存失效,影响性能。 尽量减少更新 option 的次数。
  4. 使用 Transient API 存储临时数据: 对于一些临时性的数据,可以使用 Transient API 来存储,并设置合适的过期时间。
  5. 避免查询不存在的 option: 在查询 option 之前,可以使用 option_exists() 函数来判断该 option 是否存在,避免不必要的数据库查询。

九、总结:get_option() 的智慧

get_option() 看似简单,但它充分利用了对象缓存机制,避免了重复的数据库查询,大大提高了WordPress的性能。 通过深入理解 get_option() 的源码,我们可以更好地理解WordPress的缓存机制,并编写出更高效的WordPress插件和主题。

希望今天的分享对大家有所帮助! 记住,代码的世界充满了惊喜,只要我们不断学习,勇于探索,就能发现更多的宝藏!

最后,送给大家一句话: 缓存一时爽,一直缓存一直爽!(当然,前提是要合理使用!)

谢谢大家!

发表回复

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