WordPress Transients API 入门

每个人都希望网站更快。网站越快,用户越愿意停留、继续浏览页面或购买商品。缓存是提高网站速度的一种方法:先存储信息,之后读取它,比重新连接数据库更快。打个比方,从冰箱拿零食,比开车去商店、在货架上找商品、结账,再开车回家吃,要快得多。

缓存有很多类型,包括对象缓存、浏览器缓存、页面缓存,甚至数据库缓存。每种都有相应用途,设置方式可能只是安装插件,也可能需要修改服务器配置来启用对象缓存。本文介绍 Transients API。根据使用环境,它可能采用对象缓存,也可能采用数据库缓存。

Transients API 用于在 WordPress 中跨页面加载临时保存信息。默认安装将信息保存在数据库的 options 表中。它与 Options API 很相似,但允许设置过期时间,过期后数据会被删除。

Transient 非常强大,不仅可以避免为页面内容反复请求数据库,还能加快复杂 HTML 标记生成,并缓解第三方 API 请求缓慢带来的影响。

保存 transient

使用 set_transient 保存 transient。它接受三个参数:名称、待存储数据,以及可选的以秒为单位的过期时间。设置成功返回 true。

下面设置一个一周后过期的 transient:

set_transient( 'dev-blog-transient', 'Some data I want to persist', WEEK_IN_SECONDS );

WordPress 提供多个便于计算时间的常量,使用 Transients API 时很实用,也让代码更易读。WEEK_IN_SECONDS 比 604800 更容易理解。

读取 transient

使用 get_transient 获取数据。它只接受一个参数,即 transient 名称。存在时返回数据,不存在或已过期时返回 false。

$persisted_data = get_transient( 'dev-blog-transient' );

最好使用 === 严格比较,确认数据存在并且类型一致。某些缓存值在布尔语境下可能为假,例如零;原文也将字符串“false”列作此类例子,但它们在业务中仍可能是有效数据。严格比较可以避免把有效数据误判为缓存缺失。

下面是常见的检查方式,也为缺失时重新生成 transient 留好了位置:

if ( false === ( $persisted_data = get_transient( 'dev-blog-transient' ) ) ) {
  // This will run if there is no transient or it has expired.
} 

删除 transient

可以随时通过 delete_transient 删除 transient。它也只接受名称参数,删除成功返回 true。

delete_transient( 'dev-blog-transient' );

对象缓存

要完整理解 Transients API,需要先了解 WordPress 的内存缓存,也就是对象缓存。对象缓存可以把计算成本较高的结果留在内存中,供后续使用。

典型场景是一个开销较大的 WP_Query,其结果会用到多次。缓存结果就不必反复查询数据库,从而缩短处理时间。

原文使用 WP_Object_Class 这个名称描述底层缓存类;实际交互通常通过 wp_cache_* 函数族完成。

默认对象缓存不持久,页面加载结束后,缓存内容就会消失。借助缓存插件可以实现持久化。具体设置超出本文范围,通常需要修改服务器,并添加 object-cache.php drop-in,覆盖默认对象缓存。

这与 Transients API 有什么关系?一旦使用持久化对象缓存,Transient 就不再通过 Options API 存入数据库,而改由对象缓存保存。直接收益是速度:读取内存中的数据通常远快于查询数据库。

更好的是,无需修改业务代码就能获得这些性能收益,切换都发生在底层。

Transient 的过期时间

过期时间经常被误解。Ryan McCue 在官方文档中的解释非常清楚:

大家似乎总是误解 transient 的过期机制。简而言之,过期时间是最长存活时间,没有最短存活时间。Transient 可能设置一秒后就消失,也可能 24 小时后消失,但绝不会在过期时间之后继续存在。

Ryan McCue

数据库更新、对象缓存被清空等外部因素,都可能导致数据提前消失。因此,代码必须始终考虑 transient 不存在的情况。

这也意味着,即使把过期时间设为 100 年以后,也无法保证数据会一直保留到那个时候。

始终设置过期时间

设置 transient 时,可以省略过期时间参数,它默认是 0:

set_transient( 'dev-blog-transient', 'Some data I want to persist' ); 

原文将这一情况描述为数据不应在 0 秒之后仍可用,并提醒不要因此把它当作很短时间缓存的合适做法。在 transient 保存到数据库时,它有一个潜在的不良副作用。

Transient 底层通过 Options API 写入数据库。查看源代码会发现,没有过期时间的 transient 会将对应选项设为自动加载:

$autoload = 'yes';
if ( $expiration ) {
    $autoload = 'no';
    add_option( $transient_timeout, time() + $expiration, '', 'no' );
}
$result = add_option( $transient_option, $value, '', $autoload );

这意味着每次页面请求都会自动载入该数据,即使根本没有用到。设想每次加载页面都读取 1000 个包含大型数据集的 transient,网站性能可能受到巨大影响。

除了部署对象缓存,最简单的避免方式就是始终设置过期时间,即使只有一秒:

set_transient( 'dev-blog-transient', 'Persisted but not autoloaded data', 1 ); 

如果没有持久化对象缓存,只想在一次页面加载期间复用数据,而不需要 transient,可以使用 wp_cache_set() 和 wp_cache_get()。页面加载结束后,这些数据会消失。

什么时候使用 transient

这取决于代码在哪里运行,以及你是否了解运行环境。

有时,代码运行在自己的站点或客户站点上,服务器配置已知,可以确定是否启用了 Redis、Memcached 等持久化对象缓存。此时可以使用 Transients API,也可以使用 wp_cache_*,因为 transient 已经保存在对象缓存中。

如果公开发布插件或主题,就无法知道用户的服务器环境。这种情况下,使用 Transients API 更合适,因为没有持久化对象缓存时,它仍可写入数据库。

简而言之:需要在不同服务器环境中都能跨请求保存数据时,使用 Transients API。

代码示例

读取或重新生成 transient 数据

最常见的用法是检查 transient 是否存在,不存在则生成。

下面读取文章 ID 列表。如果 transient 返回 false,就执行查询,并通过 set_transient 只保存查询返回的 ID 数组。关键在于,读取缓存和最终生成的 ID 列表都使用同一变量 $persisted_data,保证后续代码无论缓存是否命中都能正常工作。

if ( false === ( $persisted_data = get_transient( 'dev-blog-post-id-list' ) ) ) {
    // The transient doesn't exist or has expired so you need to regenerate
    $query = new WP_Query( array( 'posts_per_page' => 25, 'fields' => 'ids' ) );
    if ( $query->have_posts() ) {
        $persisted_data = $query->posts;
    } else {
        $persisted_data = array();
    }
    set_transient( 'dev-blog-post-id-list', $persisted_data, WEEK_IN_SECONDS );
}

预热缓存

缓存预热有助于让访客获得更好的性能体验。其本质是:网站发生变化时,重新生成相关 transient,使用户访问时数据已经准备好。

下面在文章保存时预热 transient:

add_action(
    'save_post',
    function () {
        // The post was saved.
        $query = new WP_Query( array( 'posts_per_page' => 25, 'fields' => 'ids' ) );
        if ( $query->have_posts() ) {
            $persisted_data = $query->posts;
        } else {
            $persisted_data = array();
        }
        set_transient( 'dev-blog-post-id-list', $persisted_data, WEEK_IN_SECONDS );
    },
);

存储 HTML 标记

Transient 不一定只保存数据。有时预先生成并缓存 HTML 更高效。基本思路相同:检查 transient,不存在时重新生成标记并保存。

下面读取最近五篇文章,生成包含文章标题和作者名称的列表:

if ( false === ( $markup = get_transient( 'stored_markup' ) ) ) {
    $query  = new WP_Query( array( 'posts_per_page' => 5 ) );
    $markup = '';
    if ( $query->have_posts() ) {
        $markup .= '<ul>';
        foreach ( $query->posts as $post ) {
            $markup .= '<li>';
            $markup .= get_the_title( $post->ID ) . ' | ';
            $markup .= get_the_author_meta( 'nicename', $post->post_author );
            $markup .= '</li>';
	}
	$markup .= '</ul>';
	set_transient( 'stored_markup', $markup, WEEK_IN_SECONDS );
    }
}

存储 API 响应

第三方 API 响应非常适合存入 transient。通常没有必要让每次页面加载都发起新请求;保存响应还能缓解 API 限流或响应缓慢的问题。

下面请求一个第三方表情包生成服务,并把返回数据保存在 transient 中:

if ( false === ( $api_data = get_transient( 'stored_memes' ) ) ) {

	// Make the request to the API.
	$response = wp_remote_get( 'https://api.imgflip.com/get_memes', array( 'headers' => array( 'Content-Type'  => 'application/json') ) );

	// Ensure that there is a response.
	if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {

		// Retrieve the body of the request.
		$body = wp_remote_retrieve_body( $response );

		// Process API data.
		$api_data_json = json_decode( $body, true );
		$api_data      = $api_data_json['data']['memes'];

		// Store the results for a maximum of 1 hour.
		set_transient( 'stored_memes', $api_data, MINUTE_IN_SECONDS * 60 );
	}
}

你是否以有趣或出人意料的方式使用过 transient?欢迎在评论中分享示例。

感谢 @greenshady 和 @milana_cap 对原文的审阅。


原文:An introduction to the Transients API。作者/维护者:Ryan Welcher。本文为原文的中文译文;代码保留原文内容。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容