批量查询拖垮后台:我用三行代码把 2000 次 `get_post_meta` 压成 1 次,顺便给静态资源上了道"指纹锁"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 77 浏览 0 回复

上周帮客户排查一个后台加载 12 秒的问题,Profiler 一拉,2000 多个 post 的列表页,每个都单独走了 get_post_meta,SQL 日志刷出满屏的 SELECT meta_key...。这场景太经典了,记录一下我现在的"三板斧"。

第一斧:把 N+1 拍死在 WP_Query

很多人知道 update_postmeta_cache,但真写查询时还是忘。我的习惯是凡涉及列表,先强制预加载:

$query = new WP_Query( [
    'post_type'      => 'product',
    'posts_per_page' => 50,
    'meta_query'     => [ /* ... */ ],
    'update_postmeta_cache' => true,  // 默认其实开了,但显式写上防接手的人乱改
    'update_post_term_cache' => true,
    'no_found_rows'  => true,         // 不需要分页总数时,省一次 COUNT(*)
] );

更隐蔽的坑是 get_posts() 默认 suppress_filters = true,有些缓存插件的预加载逻辑会被绕过去。我现在列表查询统一用 WP_Query,不再图省事。

第二斧:给元数据造个"热层"

有些字段是高频只读的,比如商品的 SKU、库存状态。我做了个轻量封装,比直接上对象缓存省点事:

class SKU_Cache {
    private static $hot = [];  // 请求级内存缓存
    
    public static function get( $product_id ) {
        if ( isset( self::$hot[ $product_id ] ) ) {
            return self::$hot[ $product_id ];
        }
        
        $cached = wp_cache_get( "sku_{$product_id}", 'my_plugin' );
        if ( false !== $cached ) {
            self::$hot[ $product_id ] = $cached;
            return $cached;
        }
        
        $sku = get_post_meta( $product_id, '_sku', true );
        self::$hot[ $product_id ] = $sku;
        wp_cache_set( "sku_{$product_id}", $sku, 'my_plugin', HOUR_IN_SECONDS );
        
        return $sku;
    }
}

三层结构:内存 > 对象缓存 > 数据库。请求内重复访问零开销,跨请求走 Redis/Memcached。注意 wp_cache_set 的 group 要自定义,别蹭 posts group,不然被其他插件 wp_cache_flush 误伤。

第三斧:静态资源的"版本指纹"别再用时间戳

以前图省事写 ?ver=,CDN 缓存直接失效。现在改拿文件内容的 hash 前 8 位:

function my_plugin_asset_url( $file ) {
    $path = plugin_dir_path( __FILE__ ) . 'assets/' . $file;
    $url  = plugin_dir_url( __FILE__ ) . 'assets/' . $file;
    
    if ( file_exists( $path ) ) {
        $ver = substr( md5_file( $path ), 0, 8 );
        return add_query_arg( 'ver', $ver, $url );
    }
    
    return $url; // fallback
}

内容不变 hash 不变,CDN 能长期缓存;内容一变 hash 变,浏览器自动拉新。配合 wp_enqueue_script 时:

wp_enqueue_script(
    'my-admin-chart',
    my_plugin_asset_url( 'js/chart.js' ),
    [ 'jquery' ],
    null,  // ver 已经拼在 URL 里了,这里给 null 防重复
    true
);

有个细节:如果用了 SCRIPT_DEBUG,我会把 hash 换成 filemtime,方便开发时刷新即时生效,生产环境切回 md5。

踩过的坑

1. md5_file 对大文件(比如 2MB 的 JS bundle)有点慢,我现在超过 500KB 的走 filemtime,或者构建时预生成 hash 写进 manifest.json
2. 对象缓存没装时 wp_cache_get 永远返回 false,逻辑里别假设它一定命中。
3. update_postmeta_cache 只预加载 postmeta,自定义表的元数据得自己写 cache_posts_meta 类似的批量填充。

现在那个客户的后台从 12 秒降到 800ms,Profiler 里最慢的变成 WordPress 核心本身的翻译加载了——这我管不了,至少插件这块问心无愧。

你们处理过更狠的 N+1 场景吗?比如跨表 join 或者 get_users 连带用户元数据的情况,有没有更骚的预加载姿势?

评论0
回复 · 0
还没有回复
微信客服 微信客服