本文目录导读:

在 Laravel 中,是的,使用随机过期时间(加随机偏移量)是预防缓存雪崩最常用且有效的策略之一。
但你问的“随机过期”通常不是完全随机到离谱,而是 “基础过期时间 + 随机偏移量”。
下面我详细说明在 Laravel 中如何实现、以及为什么这样做能解决雪崩。
什么是缓存雪崩?
- 场景:大量缓存(例如所有热门文章)设置了相同的过期时间(如 60 分钟)。
- 问题:60 分钟一到,所有这些缓存同时过期,此时突然有大量并发请求同时涌入数据库,导致数据库瞬间崩溃。
Laravel 中如何实现随机过期?
Laravel 自带的 Cache 门面支持你动态计算过期时间。
手动计算随机偏移(推荐)
这是最灵活的方式,在设置缓存时,用一个随机函数给过期时间加上一个“抖动”。
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
$baseTTL = 3600; // 基础过期时间:1小时
$randomOffset = rand(0, 300); // 随机偏移:0 到 5分钟
$expiresAt = $baseTTL + $randomOffset;
// 方法1:使用 addSeconds(Carbon)
$expiresAtTime = Carbon::now()->addSeconds($expiresAt);
Cache::put('article_'.$id, $data, $expiresAtTime);
// 方法2:使用分钟(更简洁)
Cache::put('article_'.$id, $data, 60 + rand(0, 5)); // 60分钟 + 随机0-5分钟
核心原理:每个缓存的过期时间不再是完全一致的 60分钟,而是分布在 60-65分钟 之间,这样就不会在同一秒内造成所有缓存同时失效。
使用 remember + 随机(最常用)
如果你使用 remember(缓存中有则取,无则执行回调并缓存),同样可以应用随机:
use Illuminate\Support\Facades\Cache;
$value = Cache::remember('user_profile_'.$userId, now()->addMinutes(60 + rand(0, 10)), function () {
return DB::table('users')->find($userId);
});
全局配置(不推荐,但能实现)
如果你的应用所有缓存都使用同一个 TTL,且希望一次性解决,可以在 config/cache.php 里统一设置 TTL,但通常我们按业务逻辑手动控制更合理。
为什么随机过期能解决雪崩?
- 没有随机:所有 Key 的 TTL 相同,比如都是
60分钟,时间一到,同时过期,同时回源查询数据库。 - 有随机:所有 Key 的 TTL 分布在
60-70分钟之间,缓存是逐批次过期和重建的,数据库负载被均匀分散,避免了瞬间的高并发冲击。
随机过期的局限性(什么时候不够用?)
虽然随机过期是第一道防线,但它无法解决所有雪崩场景:
- 热点 Key 雪崩:如果一个超级热门的 Key(比如微博热搜榜)它自己过期了,且它的 TTL 是随机的,但它在过期的一瞬间,依然会有海量请求打进来,随机过期对单个 Key 重建时的瞬间流量无效。
- 缓存服务重启:Redis 宕机后重启,所有 Key(不管是否随机)都消失了,这时所有请求会直接打向数据库。
针对以上局限的补充方案(组合拳)
为了防止随机过期失效的极端情况,Laravel 项目中常用的组合策略是:
-
策略 1:随机过期 + 互斥锁
- 原理:当缓存过期时,只让一个请求去数据库查询(获得锁),其他请求等待或返回旧数据。
- Laravel 实现:使用
Cache::lock()。
use Illuminate\Support\Facades\Cache; public function getHotArticles() { $cacheKey = 'hot_articles'; $ttl = 60 + rand(0, 5); // 尝试从缓存获取 $articles = Cache::get($cacheKey); if ($articles !== null) { return $articles; } // 缓存过期或不存在,尝试获取锁(防止雪崩) $lock = Cache::lock($cacheKey . '_lock', 10); // 锁超时10秒 try { if ($lock->get()) { // 拿到锁的请求:重新从数据库查询并设置缓存 $articles = DB::table('articles')->where('is_hot', 1)->get(); Cache::put($cacheKey, $articles, $ttl); return $articles; } else { // 没拿到锁的请求:等待或返回旧数据(降级) // 方案A:等待重试 usleep(200000); // 等待200ms再尝试 return Cache::get($cacheKey) ?? []; // 方案B:直接返回空或错误提示(作为兜底) } } finally { $lock->release(); } } -
策略 2:缓存永不失效 + 异步刷新(热加载)
- 适合实时性不高的数据,缓存逻辑上永不过期,后台有个定时任务(如 Laravel 的 Schedule)在凌晨低峰期主动刷新缓存。
- 前台代码则永远读取缓存,不接触数据库。
-
策略 3:二级缓存(本地 + 分布式)
使用 Laravel 的多层缓存驱动(如文件 + Redis)。
| 问题 | 答案 |
|---|---|
| Laravel 缓存雪崩用随机过期吗? | 是的,这是基础且必须做的。 |
| 具体怎么做? | Cache::put(key, data, 60 + rand(0,5)) 或 now()->addMinutes(60 + rand(0,5))。 |
| 能完全解决雪崩吗? | 不能。 对于热点 Key 和缓存服务重启失效。 |
| 还要配合什么? | 互斥锁(Cache::lock)或 缓存预热。 |
先上随机过期(防集体失效),再上互斥锁(防单点重建冲击),这是 Laravel 项目中处理缓存雪崩最稳妥的“双保险”。