后台配置页"双写"陷阱:当 `update_option` 遇上手动 `wp_cache_set` 后,我的设置开始"薛定谔生效"

插件开发 1 浏览 0 回复 返回上级
---

上周给一个内部工具插件加配置页,踩了个挺隐蔽的坑——设置保存后,前台读到的值和后台显示的不一致,刷新一下又变,再刷新可能又回来。折腾一圈发现是自己手贱在配置保存逻辑里混用了两种缓存写法

先贴下最初的问题代码,精简过的:

```php // 配置保存处理 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; } ```

看起来没毛病?问题出在读取侧。另一个模块里我是这样读的:

```php 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 缓存机制足够用了,除非你有特殊过期策略需求。

```php 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 的缓存机制,自己全包。

```php 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、序列化、钩子触发,很烦,一般没必要。

一个验证技巧

怀疑缓存不一致时,可以用这段快速定位:

```php 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() 暴力清全局缓存当"修复",结果测试环境的其他插件配置也丢了,被运维追着骂。后来改成精准删除

```php // 只清自己可能污染的 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 系统不是裸数据库,它自带缓存语义,再套一层之前先想清楚到底要规避什么。

你们有没有在配置保存时遇到过"写入成功、读取失败"的玄学问题?最后发现是缓存层哪块的问题?

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