Laravel缓存穿透用空值缓存吗

wen PHP项目 20

本文目录导读:

Laravel缓存穿透用空值缓存吗

  1. 什么是缓存穿透?
  2. 核心解决方案:缓存空值
  3. 空值缓存方案的优缺点
  4. 配套措施:更完整的防穿透体系
  5. 总结:什么时候用空值缓存?

在 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 扩展包或 RedisBF 模块。
  • 适用场景:数据总量稳定且可预知的场景(如用户 ID 范围)。

2 请求限流与参数校验

  • 前端校验:拦截明显不合法的参数(如负数 ID、超长字符串)。
  • 后端限流:对同一 IP、同一用户、同一 API 进行访问频率限制(Laravel 内置 throttle 中间件)。

3 热点数据降级与熔断

  • 当数据库压力过大时,直接返回降级内容(如页面缓存或错误提示),不再查库。

4 缓存空值 + 异步更新

  • 对于被缓存为空的数据,当真实数据写入时,通过事件或队列主动删除空缓存,让下一次查询回源。

什么时候用空值缓存?

场景 建议
内部系统,数据量不大,攻击风险低 空值缓存 + 短过期时间 足够了,简单好用
高并发、对外接口、数据量大 优先使用 布隆过滤器 拦截无效请求,再配合空值缓存做保底
实时性要求极高 使用空值缓存 + 数据变更主动删除缓存(结合 Cache::forget 或事件监听)

空值缓存是 Laravel 应对缓存穿透的基操,但非银弹。 你需要为其设置短过期时间,并视业务规模用布隆过滤器主动更新来弥补其不足。

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