引言
缓存是性能优化的第一杠杆——一个设计良好的缓存能让接口从 200ms 降到 5ms。但缓存也有一堆「坑」:穿透、击穿、雪崩、双写不一致。本文讲清 PHP 项目里缓存的完整体系:从 OPcache 到 Redis,从数据结构选型到一致性方案,从 Laravel 缓存组件到分布式锁。
前置:/php-performance-tuning/(性能调优)、/php-laravel-eloquent-advanced/(ORM 与查询优化)、/php-mysql-database/(数据库)。
目录
- 1. 缓存层次:该缓存什么
- 2. Redis 数据结构选型
- 3. 读缓存的标准流程
- 4. 缓存穿透:查不到也扛住
- 5. 缓存击穿:热点失效的瞬间
- 6. 缓存雪崩:大面积同时失效
- 7. 缓存与数据库一致性
- 8. Laravel 缓存组件与分布式锁
- 9. 实战:完整缓存架构
- 10. 速查表与一句话记忆
- 延伸阅读
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 |
| Laravel | Cache::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 命令参考
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。