配置页"热重载"翻车记:我把 `wp_options` 当实时数据库用,结果被 object cache 的"异步回写"上了一课
上周给插件加了个"高级设置"页,需求很简单:用户改完配置,前端预览要立刻跟着变,不用手动刷新。我脑子一热,直接在前端 AJAX 里读了 `get_option`,心想这还能有延迟?结果测试同事疯狂点保存,预览区开始跳帧——保存成功提示都出来了,读回来的还是老数据。
先贴我当时以为"稳如老狗"的代码:
// admin-ajax.php 处理保存
public function save_settings() {
check_ajax_referer('my_plugin_nonce', 'nonce');
$new_value = sanitize_text_field($_POST['api_key']);
update_option('my_plugin_api_key', $new_value);
// 立刻读回来"确认"
$confirmed = get_option('my_plugin_api_key');
wp_send_json_success(['saved' => $confirmed]);
}
问题出在 `update_option` 的返回值和实际落盘是两回事。当 Memcached/Redis 这类外部 object cache 介入时,`update_option` 先写数据库,再尝试删缓存键,但删缓存本身可能失败、延迟、或者被另一个并发请求覆盖。我 `update_option` 之后马上 `get_option`,命中的是缓存层还没失效的旧值,数据库其实已经更新了。
更坑的是,我为了"优化"还在配置页初始化时加了这行:
// 别学我,这是错的
wp_cache_add('my_plugin_settings_all', get_option('my_plugin_settings_all'), '', 300);
自己造了个 5 分钟 TTL 的聚合缓存,结果 `update_option` 只清它自己的单键,我手搓的聚合键完全感知不到变化。用户保存完,列表页读聚合缓存,详情页读单键,两边数据对不上,差点被当成灵异事件。
现在的做法分了三层,把"写配置"和"读配置"彻底拆开:
1. 写路径:强制穿透 + 广播失效
public function update_setting(string $key, $value): bool {
// 先写数据库,拿到真实结果
$updated = update_option("my_plugin_{$key}", $value);
if ($updated || get_option("my_plugin_{$key}") === $value) {
// 无论 update_option 返回什么,确保缓存层干净
wp_cache_delete("my_plugin_{$key}", 'options');
// 清掉我自己造的聚合缓存
wp_cache_delete('my_plugin_settings_all', 'my_plugin');
// 发个"配置变更"信号,让其他实例也感知
do_action('my_plugin_setting_changed', $key, $value);
}
return $updated;
}
2. 读路径:按需回源,拒绝"预聚合"
public function get_setting(string $key, $default = null) {
// 不绕缓存,让 WordPress 自己处理 object cache 的 get/set
// 如果外部缓存挂了,至少能回源到 options 表
$value = get_option("my_plugin_{$key}", $default);
// 只对"高频低变"的字段做短 TTL 缓存,且缓存键和原键完全隔离
return apply_filters("my_plugin_setting_{$key}", $value);
}
3. 前端"热重载"的兜底:版本号机制
既然缓存刷新有不确定性,我在前端换了个思路。保存成功后,服务端返回一个 config_version(其实就是当前时间戳),前端所有读配置的请求都带这个版本号参数。服务端收到带版本号的请求,强制跳过缓存读最新值;不带版本号的走正常缓存路径。这样只有"刚保存完的那几次预览"会慢一点点,平时不影响性能。
// 前端保存后的预览请求
fetch(ajaxurl, {
body: new URLSearchParams({
action: 'my_plugin_preview',
config_version: response.data.version // 保存时拿到的时间戳
})
})
还有个意外收获:WordPress 的 wp_cache_delete 在部分 object cache 实现里返回 false 不代表删除失败,只是"这个键本来就不存在"。我之前拿返回值判断"是否刷新成功",逻辑完全是错的。现在改成先删、再读验证、不一致就告警,至少能及时发现缓存层的异常。
你们有没有遇到过 update_option 之后 get_option 不一致的情况?是用的哪种缓存后端?我这边 Memcached 和 Redis 的表现还不太一样,Redis 偶尔会有 10-20ms 的延迟窗口,Memcached 倒是挺干脆的。

