`update_user_meta` 返回 true 但数据"原地踏步":我撞上了 WordPress 的"伪更新"与缓存穿透墙

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

上周给积分插件加"每日签到重置"功能,踩了个让人怀疑人生的坑: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 会导致短暂不一致(用户连点两次),但删缓存又担心击穿。目前折中方案是"删缓存 + 互斥锁重建",但代码复杂度上去了。有没有更优雅的姿势?

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