后台配置页"保存后刷新却回显旧值":我排查三天发现是 `update_option` 与对象缓存的"时间裂缝"

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

上周给插件加后台配置页,表单提交、数据库落库、成功提示全绿,但 F5 一刷新,页面回显的仍是老数据。Chrome 控制台干净得像没发生过任何事,数据库里新值明明在。折腾到第三天凌晨,才意识到是对象缓存(object cache)在搞"影分身"。

先把当时的简化代码贴出来,问题就藏在第 7 行:

public function save_settings() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( '权限不足' );
    }
    check_admin_referer( 'my_plugin_save', 'my_plugin_nonce' );

    // 用户勾选"启用高级模式",值为 1
    $advanced_mode = isset( $_POST['advanced_mode'] ) ? 1 : 0;
    update_option( 'my_plugin_advanced_mode', $advanced_mode );  // ← 这里"成功"了

    // 立刻回写,用于同一次请求内的后续逻辑
    $this->settings['advanced_mode'] = $advanced_mode;

    add_settings_error(
        'my_plugin_messages',
        'my_plugin_message',
        '设置已保存',
        'updated'
    );
}

看起来人畜无害对吧?`update_option` 返回 true,数据库 `wp_options` 表里值也变了。但配置页的回显用的是 `get_option('my_plugin_advanced_mode')`,这个调用在 Memcached/Redis 开启的环境下,**不会**去读数据库,而是读对象缓存里的副本。而 `update_option` 的源码里有个细节:

// wp-includes/option.php 简化逻辑
function update_option( $option, $value, $autoload = null ) {
    // ... 前置检查 ...
    
    $result = $wpdb->update( $wpdb->options, ... );
    
    if ( ! wp_installing() ) {
        $notoptions = wp_cache_get( 'notoptions', 'options' );
        if ( is_array( $notoptions ) && isset( $notoptions[$option] ) ) {
            unset( $notoptions[$option] );
            wp_cache_set( 'notoptions', $notoptions, 'options' );
        }
        wp_cache_set( $option, $value, 'options' );  // ← 这里会刷新缓存
    }
    
    return $result;
}

理论上 `wp_cache_set` 会覆盖缓存,那为啥我的场景失效了?因为我用了自定义的缓存分组。插件早期为了"性能优化",把配置项塞进了自己的 group:

// 错误示范:自以为聪明的"优化"
wp_cache_set( 'my_plugin_settings', $settings, 'my_plugin_group', 3600 );

保存时只调了 `update_option`,却忘了同步清理 `my_plugin_group` 里的聚合缓存。于是数据库是新值,对象缓存是老值,页面回显从缓存读,用户看到的就是"保存失败"的假象。更坑的是,本地开发环境没开对象缓存,这个问题在线上才暴露。

现在的做法是分层管理,统一出口

class My_Plugin_Settings {
    private const CACHE_GROUP = 'my_plugin';
    private const CACHE_KEY   = 'settings_v2';  // 加版本号,方便强制失效

    public function get( $key, $default = null ) {
        $settings = wp_cache_get( self::CACHE_KEY, self::CACHE_GROUP );
        
        if ( false === $settings ) {
            $settings = get_option( 'my_plugin_settings', [] );
            wp_cache_set( self::CACHE_KEY, $settings, self::CACHE_GROUP, 600 );
        }
        
        return $settings[$key] ?? $default;
    }

    public function set( $key, $value ) {
        $settings = $this->get_all();  // 先读当前全量
        $settings[$key] = $value;
        
        // 数据库与缓存必须原子性处理
        $updated = update_option( 'my_plugin_settings', $settings );
        
        if ( $updated ) {
            wp_cache_set( self::CACHE_KEY, $settings, self::CACHE_GROUP, 600 );
        }
        
        return $updated;
    }

    public function flush() {
        wp_cache_delete( self::CACHE_KEY, self::CACHE_GROUP );
    }
}

另外给保存按钮加了"硬刷新"的兜底:提交成功后带一个 `?my_plugin_flush=1` 参数,后端检测到就执行一次 `wp_cache_flush()` 的定向清理(不是全站 flush,只清自己的 group)。虽然有点粗暴,但在复杂托管环境(比如某些强制缓存 1 小时的平台)里能救命。

最后列几个排查这类问题的快捷命令,WP-CLI 在手会快很多:

# 看某个 option 的当前缓存值
wp cache get my_plugin_settings options

# 强制删除,测试是否缓存导致
wp cache delete my_plugin_settings options

# 直接读数据库绕过缓存
wp db query "SELECT option_value FROM wp_options WHERE option_name='my_plugin_settings'"

对象缓存不是洪水猛兽,但"半吊子优化"比不用还危险。如果你也遇到过保存成功、回显失败的玄学问题,先问问自己:数据到底存在几份?哪一份才是"真相"。

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