本文目录导读:

在 Laravel 中,使用空值缓存(缓存空对象)是应对缓存穿透的常用且有效方案,但不能仅依赖此一种措施。
下面为你详细拆解 Laravel 中缓存穿透的处理策略,包括空值缓存的具体实现、优缺点以及配套方案。
什么是缓存穿透?
现象:请求查询一个数据库中也不存在的数据,每次请求都会绕过缓存,直接打到数据库上。
危害:如果有大量此类请求(如恶意攻击),数据库会承受巨大压力,甚至宕机。
示例:查一个 id = -1 的用户,缓存中无此 key,数据库中也无此记录,每次请求都会查库。
核心解决方案:缓存空值
这是实现最简单、效果最直接的方式。
核心逻辑:当从数据库查询结果为空(null)时,仍然将这个“空结果”存入缓存,并设置一个较短的过期时间(如 60 秒)。
1 在 Laravel 中的实现示例
use Illuminate\Support\Facades\Cache;
use App\Models\User;
public function findUser(int $id): ?User
{
// 1. 尝试从缓存获取,key 存在且值为 null,也会返回 null。
$user = Cache::get('user:' . $id);
if ($user !== null) {
// 缓存命中(包括缓存了“空”的情况?不,如果我们缓存了 null,这里 $user null)
// 注意:Cache::get key 不存在返回 null,key 存在但值是 null,也返回 null。
// 所以需要区分“缓存未命中”和“缓存了空值”。
return $user;
}
// 2. 检查是否缓存了“空值”的标记
if (Cache::has('user:empty:' . $id)) {
return null; // 命中空值缓存
}
// 3. 缓存未命中,查询数据库
$user = User::withTrashed()->find($id); // 假设允许查软删除的
// 4. 无论数据库是否查到,都存入缓存
if ($user) {
// 查到数据,正常缓存,过期时间可以长一些
Cache::put('user:' . $id, $user, 3600); // 1小时
} else {
// 没有查到,缓存一个“空值标记”,过期时间很短
Cache::put('user:empty:' . $id, true, 60); // 60秒
// 或者直接缓存 null 并用特殊逻辑判断,但推荐用额外标记
}
return $user;
}
更简洁的写法(直接缓存 null):
public function findUser(int $id): ?User
{
// 使用 remember 方法:如果缓存中 key 存在,直接返回;否则执行闭包,并将结果缓存
return Cache::remember('user:' . $id, $expireTime, function () use ($id) {
$user = User::find($id);
// 即使为 null,也返回,Laravel 会缓存 null
// 但问题来了:如果数据库新增了这条记录,缓存中的 null 还在,会查不到。
// 所以推荐使用下面带空值标记的方案
return $user;
});
}
推荐做法:使用空值标记 + 短过期时间
public function findUser(int $id): ?User
{
return Cache::remember('user:' . $id, now()->addHour(), function () use ($id) {
$user = User::find($id);
if ($user) {
return $user;
}
// 缓存一个特殊的“空值”对象(避免直接存 null,因为 null 会与“key不存在”混淆)
// 也可以存一个布尔值 true
Cache::put('user:empty:' . $id, true, now()->addMinutes(5)); // 5分钟空值缓存
return null; // 注意:这里返回 null,remember 会缓存 null,但可能有问题
// 更好的方式是 return (object)[] 这样缓存一个空对象,但处理时再转换
});
}
更健壮的做法是手动处理缓存逻辑,而不是完全依赖 remember。
空值缓存方案的优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,容易理解 | 需要额外缓存空间(但空值通常是极少数) |
| 能有效防止大量不存在数据的查询打穿 | 如果数据刚被删除或新增,在缓存过期前会出现短暂的数据不一致(命中空值) |
| 对代码侵入性小 | 必须设置较短的有效期,否则真实数据写入后读不到 |
配套措施:更完整的防穿透体系
单独使用空值缓存并不完美,建议结合以下方案:
1 布隆过滤器 (Bloom Filter)
- 原理:将数据库中的所有有效 ID 预先加载到一个二进制向量中,请求来时,先检查布隆过滤器。
- 返回“不存在”:直接拒绝,不查缓存和数据库。
- 返回“可能存在”:继续查缓存或数据库(可能有误判,但概率低)。
- 优势:防止大量无效 ID 直接冲击缓存层。
- Laravel 实现:可使用
bloomfilter扩展包或Redis的BF模块。 - 适用场景:数据总量稳定且可预知的场景(如用户 ID 范围)。
2 请求限流与参数校验
- 前端校验:拦截明显不合法的参数(如负数 ID、超长字符串)。
- 后端限流:对同一 IP、同一用户、同一 API 进行访问频率限制(Laravel 内置
throttle中间件)。
3 热点数据降级与熔断
- 当数据库压力过大时,直接返回降级内容(如页面缓存或错误提示),不再查库。
4 缓存空值 + 异步更新
- 对于被缓存为空的数据,当真实数据写入时,通过事件或队列主动删除空缓存,让下一次查询回源。
什么时候用空值缓存?
| 场景 | 建议 |
|---|---|
| 内部系统,数据量不大,攻击风险低 | 空值缓存 + 短过期时间 足够了,简单好用 |
| 高并发、对外接口、数据量大 | 优先使用 布隆过滤器 拦截无效请求,再配合空值缓存做保底 |
| 实时性要求极高 | 使用空值缓存 + 数据变更主动删除缓存(结合 Cache::forget 或事件监听) |
空值缓存是 Laravel 应对缓存穿透的基操,但非银弹。 你需要为其设置短过期时间,并视业务规模用布隆过滤器或主动更新来弥补其不足。