PHP 缓存与 Redis 实战:缓存模式、数据结构与一致性

PHP 缓存与 Redis 实战:缓存层次(OPcache/内存/Redis)、Redis 数据结构选型、缓存读穿/击穿/雪崩三大问题、缓存与数据库一致性(双写/失效)、Laravel 缓存组件、分布式锁、热点数据与计数、实际缓存架构。

引言

缓存是性能优化的第一杠杆——一个设计良好的缓存能让接口从 200ms 降到 5ms。但缓存也有一堆「坑」:穿透、击穿、雪崩、双写不一致。本文讲清 PHP 项目里缓存的完整体系:从 OPcache 到 Redis,从数据结构选型到一致性方案,从 Laravel 缓存组件到分布式锁。

前置:/php-performance-tuning/(性能调优)、/php-laravel-eloquent-advanced/(ORM 与查询优化)、/php-mysql-database/(数据库)。


目录


1. 缓存层次:该缓存什么

1.1 三层缓存

层次位置命中速度适合
OPcache进程内纳秒编译后的字节码
应用内存PHP 变量微秒单请求内复用
Redis独立进程亚毫秒跨请求/跨实例共享

1.2 什么值得缓存

值得缓存:
  ✓ 热点读多写少的查询结果
  ✓ 稳定不变的配置/字典
  ✓ 高开销计算结果(报表/聚合)
不值得:
  ✗ 频繁变化的实时数据
  ✗ 唯一性要求高的数据(订单号)

记忆:缓存分三层——OPcache 管字节码、应用内存管单请求、Redis 管跨请求共享;缓存「读多写少 + 高开销」的数据。


2. Redis 数据结构选型

2.1 五大常用结构

结构用途例子
String简单值/计数用户 token、阅读数
Hash对象字段用户资料多个字段
List队列/时间线消息队列、最新列表
Set去重/交集标签、好友关系
Sorted Set排行榜/带权重热榜、延迟队列

2.2 实际代码

use Illuminate\Support\Facades\Redis;

// String:阅读数自增
Redis::incr("post:{$id}:views");

// Hash:存用户资料(避免整存整取)
Redis::hset("user:{$id}", ['name' => $name, 'age' => 30]);
$name = Redis::hget("user:{$id}", 'name');

// Sorted Set:热榜
Redis::zadd('hot_posts', $views, $postId);
$top = Redis::zrevrange('hot_posts', 0, 9);   // Top 10

记忆:按场景选结构——String 记数、Hash 存对象、List 排队、Set 去重、Sorted Set 做排行榜/延迟队列。


3. 读缓存的标准流程

3.1 读旁路缓存(Cache Aside)

读:先查 Redis → 命中返回 → 未命中查 DB → 回填缓存 → 返回
写:先写 DB → 删缓存(或更新)→ 下次读回填

3.2 代码

function getPost($id) {
    $key = "post:{$id}";
    if ($cached = Redis::get($key)) return json_decode($cached, true);

    $post = Post::find($id);
    Redis::setex($key, 300, json_encode($post));   // 300s TTL
    return $post;
}

记忆:读旁路缓存三步——先查缓存、未命中查库回填、带 TTL;写侧先写库再失效缓存。


4. 缓存穿透:查不到也扛住

4.1 问题

查询一个不存在的 key(如恶意请求不存在的商品 ID),每次都穿透到 DB。

4.2 解法

// ① 空值缓存:查不到也写一个短 TTL 空缓存
function getPost($id) {
    $key = "post:{$id}";
    if (Redis::exists($key)) {
        $v = Redis::get($key);
        return $v === 'NULL' ? null : json_decode($v, true);
    }
    $post = Post::find($id);
    Redis::setex($key, $post ? 300 : 60, $post ? json_encode($post) : 'NULL');
    return $post;
}

// ② 布隆过滤器:查询前先过滤不存在的 key(超大规模场景)

记忆:穿透 = 查不存在的 key 直击 DB;空值也缓存(短 TTL)或布隆过滤器前置拦截。


5. 缓存击穿:热点失效的瞬间

5.1 问题

一个热点 key 恰好过期,瞬间大量并发同时查 DB。

5.2 解法:分布式锁 + 单飞

function getHotPost($id) {
    $key = "hot:{$id}";
    if ($v = Redis::get($key)) return json_decode($v, true);

    // 拿分布式锁,只有一个请求去查库,其余等待
    $lock = Redis::set($key.':lock', 1, 'EX', 10, 'NX');
    if ($lock) {
        try {
            $post = Post::find($id);                 // 单飞查库
            Redis::setex($key, 300, json_encode($post));
            Redis::del($key.':lock');
            return $post;
        } catch (Exception $e) { Redis::del($key.':lock'); throw $e; }
    }
    usleep(50000);
    return getHotPost($id);    // 稍等重试,等别的请求回填
}

记忆:击穿 = 热点 key 失效瞬间并发击库;用 SET NX 分布式锁让单飞请求回填,其余自旋等待。


6. 缓存雪崩:大面积同时失效

6.1 问题

大量 key 同一时刻过期(如统一 TTL),或 Redis 宕机,DB 瞬间被打爆。

6.2 解法

// ① TTL 加随机抖动,避免同刻失效
Redis::setex($key, 300 + random_int(0, 60), $value);

// ② 热点不设过期(由更新逻辑主动失效)
// ③ 缓存双读:一级本地缓存 + 二级 Redis(降级)
// ④ Redis 高可用:主从 + 哨兵/集群,避免单点

记忆:雪崩 = 大面积同刻失效或 Redis 宕机;TTL 加随机抖动 + 热点不设过期 + 双级缓存降级 + Redis 高可用。


7. 缓存与数据库一致性

7.1 方案对比

方案一致性风险
写库后删缓存最终一致删缓存失败→脏数据
写库后更新缓存弱并发写顺序错乱
双删 + 延迟较强复杂
Cache-aside + 消息强引入 MQ

7.2 推荐:写库后删缓存 + 延迟双删

// 写库
DB::transaction(function () use ($id) {
    Post::where('id', $id)->update(['views' => $views]);
    Redis::del("post:{$id}");                    // 第一次删
    // 延迟再删一次(防并发读回填旧数据)
    Redis::expire("post:{$id}:delay", 0);
});
// 异步延迟删(简单做法:sleep 或队列)
sleep(0.1);
Redis::del("post:{$id}");

记忆:一致性推荐「写库后删缓存 + 延迟双删」——先写库、立即删缓存、稍后延迟再删,防止并发读回填旧值;追求强一致上消息队列。


8. Laravel 缓存组件与分布式锁

8.1 统一缓存门面

use Illuminate\Support\Facades\Cache;

// 读或回填(闭包)
$posts = Cache::remember('home_posts', 300, fn () => Post::published()->get());

// 标签缓存(细粒度失效)
Cache::tags(['posts', 'user:1'])->remember('user_posts', 300, fn () => ...);
Cache::tags(['posts'])->flush();

// 分布式锁(Laravel 内置)
$lock = Cache::lock('post:1:update', 10);
if ($lock->get()) {
    // 独占操作
    $lock->release();
}

8.2 驱动配置

// config/cache.php 驱动为 redis
'default' => env('CACHE_STORE', 'redis'),

记忆:Laravel 用 Cache::remember 读回填、Cache::tags 细粒度失效、Cache::lock 分布式锁——一步到位覆盖大部分场景。


9. 实战:完整缓存架构

9.1 一个商品详情接口

请求 → 一级内存缓存(本进程,纳秒)
  → 未命中 → Redis Hash(user:1:profile)
      → 未命中 → DB + 回填
写侧 → 写 DB → 删 Redis → 发消息(异步重建)
热点/排行 → Sorted Set 定时刷新

9.2 监控与调优

// 缓存命中率监控
Redis::incr('cache:hit');
Redis::incr('cache:miss');
$rate = Redis::get('cache:hit') / (Redis::get('cache:hit') + Redis::get('cache:miss'));
// 目标 95%+;未达标查 TTL、key 设计

记忆:生产架构 = 多级缓存(内存+Redis)+ 写库删缓存 + 消息异步重建 + 命中率监控;<95% 就查 key 设计与 TTL。


10. 速查表与一句话记忆

问题解法
穿透空值缓存 / 布隆过滤器
击穿SET NX 分布式锁单飞
雪崩TTL 抖动 / 热点不设过期 / 高可用
一致性写库删缓存 + 延迟双删
选型String/Hash/List/Set/ZSet
LaravelCache::remember / tags / lock
监控命中率 95%+

一句话记忆:PHP 缓存 = 分层缓存(OPcache/内存/Redis)+ 按场景选 Redis 结构(String 记数、Hash 存对象、ZSet 排行榜)+ 读旁路三步(查缓存→查库→回填带 TTL)+ 三防(穿透用空值缓存/布隆、击穿用 SET NX 单飞锁、雪崩用 TTL 抖动+热点不设过期+高可用)+ 一致性写库删缓存+延迟双删 + Laravel 用 Cache::remember/tags/lock + 命中率监控 95%+。


延伸阅读

  • /php-performance-tuning/ — OPcache/JIT 与全栈调优
  • /php-laravel-eloquent-advanced/ — ORM 查询优化配合缓存
  • /php-mysql-database/ — 数据库索引与慢查询
  • /php-microservices-message-queue/ — 消息队列异步重建缓存
  • /php-swoole-async/ — 常驻内存与连接池
  • [[redis]] — Redis 深度专题
  • [[database]] — 数据存储设计
  • Laravel Cache 文档
  • Redis 命令参考

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「php」更多文章

  1. PHP 面向对象与设计模式:SOLID、常用模式与 Laravel 实践
  2. PHP 静态分析与代码质量:PHPStan、Psalm、Rector 与 CI 门禁
  3. PHP 部署运维实战:Nginx、PHP-FPM、Docker 与 CI/CD