`wp_insert_post` 后立刻 `get_post_meta` 返回空数组:我踩了 "事务隔离" 与 "钩子时序" 的双重陷阱
上周写个内容同步插件,逻辑贼简单:A 站发文 → 调 REST API 写到 B 站 → 立刻回写 B 站的 source_id 到 A 站做关联。结果 wp_insert_post 返回了正确的 $post_id,紧接着的 update_post_meta 也报成功,但下一秒 get_post_meta($post_id, 'source_id', true) 死活返回空。
更诡异的是:手动刷新页面,值出来了;加个 sleep(1),值也出来了。我第一反应是对象缓存作妖,wp_cache_flush() 抡了好几遍,没用。
❌ 错误写法:把"同步完成"当成了"数据落盘"
// 在 save_post 钩子里直接开干
add_action( 'save_post', function( $post_id, $post ) {
// 防止递归
if ( wp_is_post_revision( $post_id ) || defined( 'DOING_SYNC' ) ) {
return;
}
define( 'DOING_SYNC', true );
// 1. 同步到远端
$remote_id = sync_to_remote( $post );
// 2. 立刻回写关联 ID
update_post_meta( $post_id, 'source_id', $remote_id );
// 3. 校验——这里崩了
$check = get_post_meta( $post_id, 'source_id', true );
if ( empty( $check ) ) {
error_log( '回写失败!remote_id=' . $remote_id ); // 实际日志疯狂输出
}
}, 10, 2 );
问题出在哪?save_post 触发时,post 数据本身可能还在事务里(尤其外接数据库主从延迟),而 update_post_meta 走的是独立表 wp_postmeta,虽然写进去了,但 get_post_meta 优先读对象缓存——这个缓存键在 save_post 早期就被当前请求占住了,update_post_meta 内部调 wp_cache_delete 清缓存,可如果当前请求里之前有过 get_post_meta 调用(哪怕隐式的),缓存可能以"空数组"形式残留。
✅ 正确写法:绕过缓存层,或者干脆等钩子谢幕
方案一:直接查库验真身(调试/关键校验场景)
add_action( 'save_post', function( $post_id, $post ) {
if ( wp_is_post_revision( $post_id ) || defined( 'DOING_SYNC' ) ) {
return;
}
define( 'DOING_SYNC', true );
$remote_id = sync_to_remote( $post );
update_post_meta( $post_id, 'source_id', $remote_id );
// 绕过对象缓存,直接 $wpdb 验货
global $wpdb;
$raw = $wpdb->get_var( $wpdb->prepare(
"SELECT meta_value FROM {$wpdb->postmeta}
WHERE post_id = %d AND meta_key = %s",
$post_id, 'source_id'
));
if ( (int) $raw !== $remote_id ) {
// 真·写入失败,走补偿逻辑
schedule_sync_retry( $post_id );
}
}, 10, 2 );
方案二:把"善后"扔到 wp_after_insert_post(WP 5.6+),等 WordPress 自己把事务和缓存收拾干净
// 注册阶段只打标记,不干重活
add_action( 'save_post', function( $post_id ) {
set_transient( 'pending_sync_' . $post_id, true, MINUTE_IN_SECONDS );
}, 10, 1 );
// 等 WordPress 官宣"insert 彻底完成"
add_action( 'wp_after_insert_post', function( $post_id, $post, $update, $post_before ) {
if ( ! get_transient( 'pending_sync_' . $post_id ) ) {
return;
}
delete_transient( 'pending_sync_' . $post_id );
$remote_id = sync_to_remote( $post );
update_post_meta( $post_id, 'source_id', $remote_id );
// 这时候 get_post_meta 稳了,缓存层已重建
$check = get_post_meta( $post_id, 'source_id', true );
// ... 正常走
}, 10, 4 );
关键差异点
| 维度 | 错误写法 | 正确写法 |
|---|---|---|
| 钩子选择 | <code>save_post</code>(中间态) | <code>wp_after_insert_post</code>(终态) |
| 缓存假设 | 默认 <code>update_post_meta</code> 会清干净 | 显式绕过或等终态钩子 |
| 事务边界 | 可能踩中未提交数据 | 等 WP 自己收工 |
| 递归风险 | <code>define</code> 防不住多实例并发 | transient 原子标记更稳 |
额外踩的坑
1. save_post 在 Quick Edit 批量编辑时也会触发,但 wp_after_insert_post 不会——如果你的同步逻辑依赖完整 post 对象,后者更安全。
2. 如果用了外接对象缓存(Redis/Memcached),wp_cache_delete 在某些客户端版本有"延迟删除"特性,get_post_meta 的返回值可能比 $wpdb 查询旧半秒。
3. wp_after_insert_post 的第四个参数 $post_before 是判断"到底有没有实质变更"的神器,别浪费。
你们有没有在钩子接力赛里被缓存"假阴性"坑过?我这次排查了四个小时,最后发现是 save_post 和 wp_after_insert_post 之间那 50 毫秒的"认知时差"。

