PHP 怎么PHP全页缓存

wen PHP项目 2

** PHP全页缓存实战指南:从原理到Nginx层加速的终极方案

PHP 怎么PHP全页缓存


目录导读

  1. 为什么需要全页缓存? —— 动态语言的性能瓶颈
  2. PHP全页缓存的三种核心实现路径
    • 文件缓存(传统方案)
    • Memcached/Redis内存缓存
    • Nginx反向代理缓存(终极方案)
  3. 手写一个工业级PHP全页缓存类(附代码)
  4. 缓存过期策略与智能刷新机制
  5. 常见坑与性能调优(含问答)
  6. 选择适合你业务的缓存层级

为什么需要全页缓存?

当用户每次请求一个PHP页面时,服务器需要执行PHP解释器、数据库查询、模板渲染等一系列操作,即便使用PHP 8的JIT编译,一个普通CMS页面在无缓存情况下依然需要50-200ms的响应时间,当并发量达到1000+时,数据库连接数会瞬间耗尽,CPU飙升到100%。全页缓存的核心思想是:将最终生成的HTML代码直接存储,后续请求直接返回该HTML,无需再执行PHP脚本。

与片段缓存、对象缓存相比,全页缓存(Full Page Cache)具有压倒性的性能优势:

缓存类型 响应时间 数据库查询 PHP执行
无缓存 150ms 20次
对象缓存 80ms 5次
全页缓存 5-20ms 0次

PHP全页缓存的三种核心实现路径

文件缓存(最简部署) 将生成的HTML保存为.html文件,利用Web服务器(Apache/Nginx)的静态文件处理能力直接返回文件内容,优点是零CPU开销,缺点是需要手动维护缓存清理逻辑,且无法处理动态用户个性化内容。

内存缓存(Memcached/Redis) 将HTML字符串存储在内存中,配合过期时间(TTL),当用户请求时,PHP脚本先从Memcached/Redis中读取键值,如果存在则直接输出,否则重新生成,这种方式适合多服务器负载均衡场景,但会增加一层网络I/O开销。

Nginx反向代理缓存(终极性能) 在Nginx层拦截请求,如果命中缓存,Nginx直接返回静态HTML;如果未命中,Nginx通过fastcgi_pass将请求转发给PHP-FPM,处理完成后将响应加入Nginx的缓存池。这是目前大型网站的标准做法,因为PHP进程根本不会被唤起,性能损耗趋近于零。

手写一个工业级PHP全页缓存类(附代码)

以下代码实现了一个支持动态TTL、爬虫绕过、移动端分离的健壮缓存类:

<?php
class FullPageCache {
    private $cachePath;
    private $cacheTime;
    private $excludeUrls;
    public function __construct($cachePath = '/tmp/fpc/', $cacheTime = 300) {
        $this->cachePath = $cachePath;
        $this->cacheTime = $cacheTime;
        $this->excludeUrls = ['/cart/', '/login/', '/admin/'];
    }
    public function start() {
        $requestUri = $_SERVER['REQUEST_URI'];
        $hashKey = md5($requestUri . $_SERVER['HTTP_USER_AGENT'] ?? '');
        $cacheFile = $this->cachePath . $hashKey . '_' . $this->getDeviceType() . '.html';
        // 排除登录态用户? 可扩展
        if (isset($_COOKIE['session_id']) && $this->isExcludeUrl($requestUri)) {
            return;
        }
        // 检查缓存文件是否存在且未过期
        if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $this->cacheTime)) {
            echo file_get_contents($cacheFile);
            exit; // 直接结束PHP生命周期
        }
        // 启用输出缓冲区
        ob_start();
        // 注册结束回调函数,自动保存缓存
        register_shutdown_function(function() use ($cacheFile) {
            $content = ob_get_clean();
            // 仅缓存200状态码的完整页面
            if (http_response_code() === 200) {
                file_put_contents($cacheFile, $content, LOCK_EX);
            }
            echo $content;
        });
    }
    private function getDeviceType() {
        // 简单判断,可根据需求扩展
        $agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
        if (strpos($agent, 'Mobile') !== false) return 'mobile';
        if (strpos($agent, 'Tablet') !== false) return 'tablet';
        return 'desktop';
    }
    private function isExcludeUrl($uri) {
        foreach ($this->excludeUrls as $url) {
            if (strpos($uri, $url) !== false) return true;
        }
        return false;
    }
    // 静态方法用于主动删除缓存
    public static function flush($key = null) {
        $cachePath = '/tmp/fpc/';
        if ($key) {
            $files = glob($cachePath . $key . '*');
        } else {
            $files = glob($cachePath . '*.html');
        }
        foreach ($files as $file) {
            unlink($file);
        }
    }
}
// 使用方式 --- 在页面最顶部调用
$cache = new FullPageCache();
$cache->start();
?>

关键点解析

  • 使用ob_start()配合register_shutdown_function保证在任何错误发生时也能正确保存缓存
  • 通过HTTP_USER_AGENTgetDeviceType实现响应式站点缓存分离
  • 缓存文件名包含md5哈希,避免非法字符冲突
  • 主动清除时使用glob模式匹配,支持单个页面或全站刷新

缓存过期策略与智能刷新机制

全页缓存最大痛点在于“数据更新后缓存不刷新”,此时需要引入事件驱动失效机制

  1. 写操作时显式删除:当管理员在后台编辑文章、更新商品价格后,立即调用FullPageCache::flush('article_' . $id)
  2. 定时过期兜底:设定一个最大TTL(如30分钟),确保极端情况下数据也不会过于陈旧。
  3. 分层标记法:在缓存文件的头部嵌入JSON元数据(如<!-- CACHE:product_123 200 "2024-01-01 10:00" -->),后台更新时,通过正则扫描所有缓存文件,找到匹配标记后删除。
  4. Redis Pub/Sub:多服务器环境下,通过Redis频道广播一条“清理商品页”消息,所有应用服务器收到消息后执行删除操作。

常见坑与性能调优(含问答)

问:为什么我的页面缓存后,登录用户的个人信息变成别人的了?

答:因为缓存劫持了所有访问者的请求,解决方案有三种:

  • 对Cookies中的登录标识进行校验,只有未登录用户才走全页缓存;
  • 将全页缓存按用户ID拆分(如/fpc/user_1234_article_456.html),但这会大幅降低缓存命中率;
  • 使用Edge Side Includes (ESI) 技术,将用户特定区域单独请求,但实现复杂度高。

问:开启Nginx缓存和Redis缓存,效果翻倍吗?

答:不完全是,Nginx缓存主要负责“终点站”拦截,如果Nginx未命中,请求会进入PHP-FPM,此时PHP内部又从Redis读取缓存的HTML字符串,属于“二级缓存”策略,正确做法是:将Redis作为标记层,Nginx从Redis读取缓存状态,避免无用的PHP进程启动。

性能调优清单

  • 保证缓存目录使用tmpfs(内存文件系统),避免磁盘I/O
  • 将Nginx的缓存键设为$scheme://$host$request_uri,忽略Cookie
  • 对静态资源(图片、CSS)使用CDN,避免动态页面体积过大
  • 设置正确的Cache-Control响应头,让浏览器端也缓存

问:我的站点有部分动态功能(如实时库存),但大部分内容不变,如何平衡?

答:采用页面片段缓存+异步加载的组合方案,将整页拆分为若干block,数据库查询结果和登录状态等黑盒区域不缓存,而文章内容、热搜榜等静态区域全页缓存,具体通过ob_start的嵌套实现,外层缓存最终HTML,内层用<div data-block="cart">占位,前端通过AJAX获取动态部分。

选择适合你业务的缓存层级

根据业务规模,建议如下:

业务阶段 推荐方案 理由
博客/个人站 (<10k PV) 文件缓存 零依赖,部署简单
中型电商 (50-200k PV) Redis缓存 + 手动清理 支持分布式,秒级清除
大型门户 (>1M PV) Nginx代理缓存 + ESI 彻底隔离PHP运行时

核心原则:永远不要相信某一种缓存能解决所有问题。全页缓存是性能极限武器,但它必须与片段缓存、Redis对象缓存、CDN这三者形成组合拳,才能既保性能又保实时性。 请务必在测试环境压测缓存命中率,通常命中率在90%以上才值得投入。

抱歉,评论功能暂时关闭!