搜索全站
插件 17 分钟阅读

WordPress transients 堆积清理:过期瞬态与 wp_options 瘦身

后台越来越慢、wp_options 涨到几百 MB?多半是插件留下的过期 transients 没被清理。本文讲清瞬态的存储机制与失效条件,给出 SQL 清理、WP-CLI 命令与自动化定时方案。

U
UFO_PF 2018-04-01 · 阅读 17 分钟
Wo 插件
阅读提示
本文约 6,645 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:wp_options 悄悄长到了 800MB

站点跑了一年多,数据库备份从 12MB 涨到 800MB,后台操作明显发涩,PHP 内存偶尔告警。查下来 wp_options 表占了 90% 的体积,里面有二十多万行以 _transient_ 开头的记录,绝大多数是几个月前就过期的数据。

这类数据的可怕之处在于它不会自己消失,或者说它消失的条件很苛刻:WordPress 只在「有人访问这个 transient」时才顺手删掉过期的它。如果插件用 transient 缓存的是「某个好久没人看的商品的推荐列表」,那这条记录可能永远躺着。

原理分析:transient 存在哪、怎么失效

Transient(瞬态)是 WordPress 的「带过期时间的 option」,三个函数:set_transient( $key, $value, $expiration )、get_transient( $key )、delete_transient( $key )。

存储位置取决于有没有外部对象缓存:

场景 存储位置 过期行为
无持久对象缓存 wp_options 表,两行:_transient_{key}(值)与 _transient_timeout_{key}(到期时间戳) 只在被 get_transient 访问时才删除
有 Redis/Memcached 对象缓存(不写数据库) 靠缓存 TTL 自然失效
多站点 站点级同上;set_site_transient 存 _site_transient_* 同上

失效的三个条件(缺一都不会被删):

  1. 有过期时间(set_transient 的第三个参数大于 0)。传 0 表示永不过期,此时 WordPress 根本不写 timeout 行,这条记录会永久留着,而且 autoload 为 yes——每个请求启动时都被加载进内存。
  2. 有人访问它。get_transient() 里发现 timeout < time() 时才会 delete_option 删掉两行。
  3. 定时任务跑过。WordPress 4.9 起,delete_expired_transients() 挂在每日事件 wp_scheduled_delete 上,会清掉数据库里的过期瞬态——但 WP-Cron 依赖站点访问触发,低流量站点可能好几天不跑;如果 wp-config.php 里设了 DISABLE_WP_CRON 而系统 cron 又没配,就一次都不跑。

所以「装了对象缓存反而更要小心」:装了 Redis 之后 transient 不再写数据库,一旦某天 Redis 挂了或配置丢失,插件会重新往 wp_options 写,堆积速度吓人。

保姆级实操

第一步:量化问题有多少

-- 总行数与瞬态占比
SELECT COUNT(*)                                             AS total_rows,
       SUM( option_name LIKE '\_transient\_%' )             AS transient_rows,
       SUM( option_name LIKE '\_transient\_timeout\_%' )    AS timeout_rows,
       SUM( option_name LIKE '\_site\_transient\_%' )       AS site_rows
FROM wp_options;

-- 体积最大的 20 条(看是哪个插件在写)
SELECT option_name, autoload, ROUND( LENGTH(option_value)/1024, 1 ) AS kb
FROM wp_options
WHERE option_name LIKE '\_transient\_%'
ORDER BY LENGTH(option_value) DESC
LIMIT 20;

-- 已过期但还没被删的
SELECT COUNT(*) AS expired_pending
FROM wp_options
WHERE option_name LIKE '\_transient\_timeout\_%'
  AND option_value < UNIX_TIMESTAMP();

-- 永不过期的(没有对应 timeout 行,且通常 autoload=yes,最该处理)
SELECT o.option_name, o.autoload, ROUND( LENGTH(o.option_value)/1024, 1 ) AS kb
FROM wp_options o
LEFT JOIN wp_options t
       ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 13))
WHERE o.option_name LIKE '\_transient\_%'
  AND o.option_name NOT LIKE '\_transient\_timeout\_%'
  AND t.option_id IS NULL
ORDER BY LENGTH(o.option_value) DESC
LIMIT 30;

注意 SQL 里 LIKE '\_transient\_%' 的反斜杠:下划线在 LIKE 里是单字符通配符,必须转义。SUBSTRING(o.option_name, 13) 是因为 _transient_ 正好 12 个字符。

第二步:先确认再删除

-- 1) 备份 options 表(这一步不能省)
-- mysqldump -u root -p dbname wp_options > wp_options_$(date +%F).sql

-- 2) 删除已过期的:value 行与 timeout 行一起删
DELETE a, b
FROM wp_options a
JOIN wp_options b
  ON b.option_name = REPLACE( a.option_name, '_transient_timeout_', '_transient_' )
WHERE a.option_name LIKE '\_transient\_timeout\_%'
  AND a.option_value < UNIX_TIMESTAMP();

这是 MySQL 的多表删除语法:以 timeout 行为驱动表 a,通过 REPLACE 拼出对应的 value 行 b,一次删两行。删完检查:

-- 还剩下多少孤儿 value 行(有值没 timeout 的,就是永不过期的那批)
SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '\_transient\_%'
  AND option_name NOT LIKE '\_transient\_timeout\_%';

第三步:用 WP-CLI 更省事

# 列出当前瞬态(需要能连上数据库)
wp transient list --fields=name,expiration,size | head -30

# 删除全部过期瞬态
wp transient delete --expired

# 删除全部瞬态(含未过期,慎用,会强制插件重新拉取数据)
wp transient delete --all

# 只想删某一个插件的:按前缀精确清理
wp transient delete --all --network 2>/dev/null   # 多站点场景
wp db query "DELETE FROM wp_options WHERE option_name LIKE '\_transient\_mmf\_api\_%'"

# 看 autoload 总量(超过 800KB 就该清理了)
wp eval '$t=0; foreach($GLOBALS["wpdb"]->get_results("SELECT LENGTH(option_value) l FROM {$GLOBALS["wpdb"]->options} WHERE autoload=\"yes\"") as $r){$t+=$r->l;} echo round($t/1024), " KB autoloaded\n";'

# 手动触发一次官方清理
wp eval 'delete_expired_transients( true ); echo "done\n";'

wp transient delete --expired 只删过期的,比 --all 安全得多;--all 会清掉正在生效的缓存,流量大的站点执行后源站压力会短暂上升,建议在低峰做。

第四步:把清理变成常态

WP-Cron 不可靠,用系统 cron:

# /etc/cron.daily/wp-transients
#!/usr/bin/env bash
set -euo pipefail
WP="wp --path=/var/www/html --skip-plugins --skip-themes"
$WP transient delete --expired >/dev/null 2>&1 || true
$WP db query "DELETE a,b FROM wp_options a JOIN wp_options b ON b.option_name=REPLACE(a.option_name,'_transient_timeout_','_transient_') WHERE a.option_name LIKE '\_transient\_timeout\_%' AND a.option_value < UNIX_TIMESTAMP();" || true

再加一层 WP-Cron 兜底(系统 cron 没配时至少还有它):

<?php
// wp-content/mu-plugins/transient-keeper.php
add_action( 'init', function () {
    if ( ! wp_next_scheduled( 'mmf_purge_transients' ) ) {
        wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'mmf_purge_transients' );
    }
} );

add_action( 'mmf_purge_transients', function () {
    if ( function_exists( 'delete_expired_transients' ) ) {
        delete_expired_transients( true );   // true = 强制连库清理,含多站点
    }
} );

// 拦住"超大瞬态":单个值超过 512KB 的一律不写库,改由调用方降级
add_filter( 'pre_set_transient_mmf_remote_cache', function ( $value, $expiration, $transient ) {
    if ( is_string( $value ) && strlen( $value ) > 512 * 1024 ) {
        error_log( 'transient 过大,已拒绝写入: ' . $transient );
        return false;   // 返回 false 会让 set_transient 直接返回 false
    }
    return $value;
}, 10, 3 );

第五步:从源头约束插件行为

// 检查插件是否写了永不过期的瞬态(autoload 会被拖慢每个请求)
add_action( 'added_option', function ( $option ) {
    if ( str_starts_with( $option, '_transient_' ) && ! str_starts_with( $option, '_transient_timeout_' ) ) {
        $timeout = get_option( '_transient_timeout_' . substr( $option, 12 ) );
        if ( false === $timeout ) {
            error_log( '检测到永不过期的瞬态: ' . $option );
        }
    }
} );

常见踩坑

  1. 直接 wp transient delete --all 当日常维护:清掉的是正在生效的缓存,插件会集体回源,API 类插件可能瞬间触发限流。

  2. 案例: 某站清完 transient 后,一个天气插件的 API Key 调用次数在 10 分钟内涨到平时的 40 倍,被服务商临时封禁。原因是清掉的瞬态里包含它缓存的 API 响应,而该插件没有并发保护,所有并发请求同时回源。教训:清理前先确认插件是否有请求合并(locking)机制,没有的话分批清、低峰清。

  3. 忽略 autoload 才是真正的性能杀手:10 万行瞬态里,只有 autoload=yes 的那些会被每个请求加载进内存。永不过期的瞬态 autoload 默认是 yes,危害远大于过期未清理的。

  4. 装了对象缓存就以为万事大吉:Redis 里的数据掉电即失,恢复期间插件会大量回写数据库;且「对象缓存里没有、数据库里有旧 transient」这种双源状态会造成数据不一致。

  5. 误删正在进行的更新锁:core_updater.lock 是 option 不是 transient,但有些插件会把「任务进行中」标记写成瞬态,清理时正好撞上,导致任务被重复执行。清理前先看一眼 key 名。

  6. DISABLE_WP_CRON 但没配系统 cron:这是瞬态堆积最常见的根因,也是定时任务全线失效的根因。用 wp cron event list 检查 wp_scheduled_delete 的 next_run 是不是早就过期了。

  7. 只删 value 行不删 timeout 行:留在表里的 timeout 行同样占用行数,wp_options 的 option_name 索引也会变大。

总结

判断标准:wp_options 行数异常、体积超过 50MB、autoload 总量超过 800KB,就该查瞬态了。

行动清单:先用 SQL 量化(总数、过期未清数、永不过期数、体积 Top20);备份 wp_options 表;用多表 DELETE 或 wp transient delete --expired 清理过期项;重点处理「永不过期 + autoload=yes」的那批;配系统 cron 每日清理,WP-Cron 只做兜底;给大瞬态加写入体积上限;装了对象缓存的站点也要定期查数据库,别只看 Redis。WordPress 数据库性能优化

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。