后台配置页"双写"陷阱:当 `update_option` 遇上手动 `wp_cache_set` 后,我的设置开始"薛定谔生效"
上周给一个内部工具插件加配置页,踩了个挺隐蔽的坑——设置保存后,前台读到的值和后台显示的不一致,刷新一下又变,再刷新可能又回来。折腾一圈发现是自己手贱在配置保存逻辑里混用了两种缓存写法。
先贴下最初的问题代码,精简过的:
// 配置保存处理
public function save_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
check_admin_referer( 'my_plugin_save', 'my_plugin_nonce' );
$new_value = sanitize_text_field( $_POST['api_key'] );
// ① 写数据库
update_option( 'my_plugin_api_key', $new_value );
// ② 顺手刷新内存缓存(想着减少一次查询)
wp_cache_set( 'my_plugin_api_key', $new_value, 'my_plugin', HOUR_IN_SECONDS );
// ③ 给前端回显用的瞬态缓存也更新
set_transient( 'my_plugin_api_key_cached', $new_value, HOUR_IN_SECONDS );
wp_redirect( admin_url( 'admin.php?page=my-plugin&saved=1' ) );
exit;
}
看起来没毛病?问题出在读取侧。另一个模块里我是这样读的:
public function get_api_key() {
// 先查对象缓存
$cached = wp_cache_get( 'my_plugin_api_key', 'my_plugin' );
if ( false !== $cached ) {
return $cached;
}
// 回退到 option
$value = get_option( 'my_plugin_api_key' );
// 回填缓存
wp_cache_set( 'my_plugin_api_key', $value, 'my_plugin', HOUR_IN_SECONDS );
return $value;
}
坑来了:update_option 内部已经会更新对象缓存(非持久化缓存环境下是 alloptions 或单独 cache key,持久化环境下看你用的什么后端)。我手动又 wp_cache_set 一次,key 的命名空间/前缀对不上——WordPress 默认的 option 缓存 key 是 my_plugin_api_key 不带 group,或者带 options group(取决于 wp_load_alloptions 的加载策略),而我手写的 group 是 my_plugin。
结果是:
get_option读的是 WordPress 管理的缓存 → 新值 ✓wp_cache_get( 'my_plugin_api_key', 'my_plugin' )读的是我手写的缓存 → 可能旧值 ✗- 两个 key 在不同 group 里,互不相干,同一个数据有了两条缓存命
更骚的是,我用的 Redis Object Cache,flush 时机不一致。后台保存后重定向带 ?saved=1,页面渲染走 get_option 读到新值;但某个 AJAX 心跳或定时任务走了 wp_cache_get 那条线,拿到旧值,反手又把旧值写回瞬态缓存。用户体感就是"保存了,但一会儿变回去"。
我的修正方案:统一读写口径
方案 A(推荐):别在 option 上再套一层手动缓存,WordPress 的 option 缓存机制足够用了,除非你有特殊过期策略需求。
public function save_settings() {
// ... 校验逻辑不变
$new_value = sanitize_text_field( $_POST['api_key'] );
update_option( 'my_plugin_api_key', $new_value );
// 只清不建,让下次读取自然回填
delete_transient( 'my_plugin_api_key_cached' );
wp_redirect( admin_url( 'admin.php?page=my-plugin&saved=1' ) );
exit;
}
public function get_api_key() {
// 直接走 option,让 WordPress 自己管缓存层
return get_option( 'my_plugin_api_key', '' );
}
方案 B(确实需要自定义缓存策略时):彻底绕过 update_option 的缓存机制,自己全包。
public function save_settings() {
// ... 校验
global $wpdb;
$new_value = sanitize_text_field( $_POST['api_key'] );
// 直接写表,不触发 update_option 的钩子与缓存
$wpdb->update(
$wpdb->options,
[ 'option_value' => $new_value ],
[ 'option_name' => 'my_plugin_api_key' ]
);
// 自己管的两层缓存统一刷新
wp_cache_set( 'my_plugin_api_key', $new_value, 'my_plugin', HOUR_IN_SECONDS );
set_transient( 'my_plugin_api_key_cached', $new_value, HOUR_IN_SECONDS );
wp_redirect( admin_url( 'admin.php?page=my-plugin&saved=1' ) );
exit;
}
但方案 B 要处理 autoload、序列化、钩子触发,很烦,一般没必要。
一个验证技巧
怀疑缓存不一致时,可以用这段快速定位:
add_action( 'admin_init', function() {
if ( ! isset( $_GET['debug_cache'] ) ) return;
$opt = get_option( 'my_plugin_api_key' );
$cache_default = wp_cache_get( 'my_plugin_api_key' ); // 默认 group
$cache_custom = wp_cache_get( 'my_plugin_api_key', 'my_plugin' );
wp_die( sprintf(
'option: %s | cache_default: %s | cache_custom: %s',
var_export( $opt, true ),
var_export( $cache_default, true ),
var_export( $cache_custom, true )
) );
} );
三个值对不上,就说明有"双写"或者 group 错配。
额外踩的半个坑
我一开始在保存后用 wp_cache_flush() 暴力清全局缓存当"修复",结果测试环境的其他插件配置也丢了,被运维追着骂。后来改成精准删除:
// 只清自己可能污染的 key
wp_cache_delete( 'my_plugin_api_key' );
wp_cache_delete( 'my_plugin_api_key', 'my_plugin' );
wp_cache_delete( 'my_plugin_api_key_cached' );
wp_cache_delete( 'alloptions', 'options' ); // 如果 autoload=yes 要清这个
但最干净的还是别制造多份缓存。WordPress 的 option 系统不是裸数据库,它自带缓存语义,再套一层之前先想清楚到底要规避什么。
你们有没有在配置保存时遇到过"写入成功、读取失败"的玄学问题?最后发现是缓存层哪块的问题?

