`update_user_meta` 返回 true 但数据"原地踏步":我撞上了 WordPress 的"伪更新"与缓存穿透墙
上周给积分插件加"每日签到重置"功能,踩了个让人怀疑人生的坑:update_user_meta 明明返回 true,数据库里值却纹丝不动。更骚的是,刷新页面显示的还是旧值,但直接跑 SQL 查库又是新的。
先上我的"自信代码",当时觉得稳如老狗:
// ❌ 错误写法:被"伪更新"和对象缓存联手演了
function reset_daily_signin( $user_id ) {
$today = current_time( 'Ymd' );
// 这里埋了第一颗雷:新旧值相同,update_user_meta 直接短路
$updated = update_user_meta( $user_id, 'last_signin_date', $today );
// 返回 true,但可能根本没写库!
return $updated;
}
// 调用场景:用户今天签过了,我想强制重置测试
reset_daily_signin( 42 ); // 返回 true,再查还是 20240916
问题一:update_user_meta 的"伪更新"机制。如果新值和旧值(经过 maybe_serialize 后)完全相等,它会直接返回 true 但跳过数据库写入,连缓存都不刷新。我的测试场景里,昨天和今天的日期字符串恰好相同——因为我在测试环境手动把日期写死了。
问题二:对象缓存的"幽灵读"。即使真的更新了,如果外部有对象缓存(Redis/Memcached)且没正确失效,get_user_meta 会优先读缓存。我本地装了 Redis 对象缓存插件,但完全忘了这茬。
问题三(最隐蔽):我同时用了 wp_cache_set 自己加了一层"业务缓存",键名和 WordPress 内部的 user_meta 缓存键撞了前缀,导致互相覆盖。
折腾两小时后,重构的写法:
// ✅ 正确写法:强制刷新 + 缓存穿透防护 + 原子操作
function reset_daily_signin_safe( $user_id, $force = false ) {
$meta_key = 'last_signin_date';
$today = current_time( 'Ymd' );
// 1. 先清缓存,防止读到脏数据做比较
wp_cache_delete( $meta_key, 'user_meta' );
// 2. 强制模式:先 delete 再 add,绕过 update 的短路逻辑
if ( $force ) {
delete_user_meta( $user_id, $meta_key );
$result = add_user_meta( $user_id, $meta_key, $today, true );
} else {
// 3. 普通模式:用 raw 值比较,避免 maybe_serialize 的"相等陷阱"
$old_value = get_user_meta( $user_id, $meta_key, true );
$result = ( $old_value !== $today )
? update_user_meta( $user_id, $meta_key, $today )
: true; // 明确返回,不隐式依赖
}
// 4. 再清一次,确保对象缓存和本地缓存都失效
wp_cache_delete( $meta_key, 'user_meta' );
wp_cache_delete( "user_meta_{$user_id}", 'users' ); // 部分缓存插件用的聚合键
// 5. 验证回读,不是信不过自己,是信不过缓存
$verify = get_user_meta( $user_id, $meta_key, true );
if ( $verify !== $today ) {
// 日志+降级:直接写 options 表做兜底,或触发缓存重建
do_action( 'signin_meta_sync_failed', $user_id, $today, $verify );
}
return $result && ( $verify === $today );
}
几个血泪细节:
1. update_user_meta 的返回值别盲信
返回 true 有三种可能:真的更新了、新旧值相同被短路了、或者 meta_id 存在但值没变。业务上如果需要"确认写入",必须做验证回读。
2. 对象缓存的键空间要隔离
我之前自己封了个 wp_cache_set( "user:{$user_id}:signin", ... ),结果 WordPress 内部用的是 user_meta:{$meta_key},虽然键名不同,但某些对象缓存插件会把 user_meta 做前缀聚合清理,导致我的业务缓存被连带误伤。现在全部加上插件前缀:myplugin_user_signin_{$user_id}。
3. 时区陷阱让"同一天"变成"昨天"
最初用 date( 'Ymd' ),服务器 UTC,用户东八区,晚上八点签到算"明天"。改成 current_time( 'Ymd' ) 才对齐 WordPress 的时区设置。但这函数在 wp-includes/functions.php 里依赖 wp_timezone(),如果在 plugins_loaded 之前调用可能时区未初始化——幸好我的逻辑挂在 init 之后。
4. 批量重置时的缓存雪崩
测试时写了个循环给 500 用户重置,Redis 的 delete QPS 飙高。后来改成:
// 批量操作:先禁用缓存写回,最后统一 flush
wp_suspend_cache_addition( true );
// ... 批量 update_user_meta ...
wp_suspend_cache_addition( false );
wp_cache_flush(); // 或者更精细的按组清理
最后留个开放问题:你们处理 user_meta 缓存一致性时,是倾向于"写即删缓存"还是"设置 TTL 自动过期"?我在高并发签到场景里,TTL 会导致短暂不一致(用户连点两次),但删缓存又担心击穿。目前折中方案是"删缓存 + 互斥锁重建",但代码复杂度上去了。有没有更优雅的姿势?

