在Laravel项目开发中,缓存雪崩是缓存使用过程中需要重点防范的问题。当大量缓存键在同一时间过期,或者缓存服务突然不可用,所有请求都会直接落到数据库上,导致数据库压力骤增甚至宕机,最终影响整个服务的可用性。下面介绍几种在Laravel中避免缓存雪崩的实用方法。

设置随机过期时间
缓存雪崩最常见的诱因是大量缓存设置了相同的过期时间,过期时刻所有请求同时穿透到数据库。解决这个问题最简单的方法就是给缓存过期时间增加一个随机值,避免同时过期。
在Laravel中设置缓存时,可以通过Cache::put方法结合随机函数来生成过期时间,示例如下:
<?php
namespace AppServices;
use IlluminateSupportFacadesCache;
class UserService
{
/**
* 获取用户列表,带随机过期时间缓存
*/
public function getUserList()
{
$cacheKey = 'user_list';
// 基础过期时间30分钟,加上0-300秒的随机值,避免同时过期
$baseTtl = 1800;
$randomTtl = rand(0, 300);
$ttl = $baseTtl + $randomTtl;
// 尝试从缓存获取
$data = Cache::get($cacheKey);
if (!is_null($data)) {
return $data;
}
// 缓存不存在,查询数据库
$data = AppModelsUser::where('status', 1)->get()->toArray();
// 存入缓存,设置随机过期时间
Cache::put($cacheKey, $data, $ttl);
return $data;
}
}
使用互斥锁防止缓存击穿引发的雪崩
当某个热点缓存过期时,大量请求同时查询这个缓存,都会发现缓存不存在,然后同时去查询数据库,这种情况会加剧雪崩的风险。可以通过互斥锁的方式,让只有一个请求去查询数据库,其他请求等待缓存更新完成后再获取缓存。
Laravel中可以结合Redis的setnx命令实现简单的互斥锁,示例如下:
<?php
namespace AppServices;
use IlluminateSupportFacadesCache;
use IlluminateSupportFacadesRedis;
class ArticleService
{
/**
* 获取热点文章详情,带互斥锁
*/
public function getHotArticle($articleId)
{
$cacheKey = 'hot_article_' . $articleId;
$lockKey = 'lock_hot_article_' . $articleId;
$lockExpire = 10; // 锁过期时间10秒
// 先尝试从缓存获取
$data = Cache::get($cacheKey);
if (!is_null($data)) {
return $data;
}
// 缓存不存在,尝试获取锁
$lock = Redis::setnx($lockKey, 1);
if ($lock) {
// 获取锁成功,设置锁过期时间,防止死锁
Redis::expire($lockKey, $lockExpire);
// 查询数据库
$data = AppModelsArticle::where('id', $articleId)->where('is_hot', 1)->first();
if ($data) {
$data = $data->toArray();
// 存入缓存,设置随机过期时间
$ttl = 3600 + rand(0, 600);
Cache::put($cacheKey, $data, $ttl);
}
// 释放锁
Redis::del($lockKey);
return $data;
} else {
// 获取锁失败,等待100毫秒后重试
usleep(100000);
return $this->getHotArticle($articleId);
}
}
}
缓存预热与持久化策略
在系统启动或者缓存大量失效前,提前将热点数据加载到缓存中,也就是缓存预热,可以有效避免缓存同时失效的问题。另外,配置缓存的持久化策略,比如Redis开启RDB或者AOF持久化,即使缓存服务重启,也能快速恢复数据,减少雪崩的影响。
Laravel中可以通过命令行来实现缓存预热,示例如下:
<?php
namespace AppConsoleCommands;
use IlluminateConsoleCommand;
use IlluminateSupportFacadesCache;
use AppModelsArticle;
class CacheWarmupCommand extends Command
{
protected $signature = 'cache:warmup';
protected $description = '预热热点缓存数据';
public function handle()
{
$this->info('开始预热缓存数据');
// 预热热点文章缓存
$hotArticles = Article::where('is_hot', 1)->limit(100)->get();
foreach ($hotArticles as $article) {
$cacheKey = 'hot_article_' . $article->id;
$ttl = 3600 + rand(0, 600);
Cache::put($cacheKey, $article->toArray(), $ttl);
}
$this->info('缓存预热完成');
}
}
可以将这个命令配置到系统的定时任务中,或者在服务部署完成后手动执行,提前加载热点数据到缓存。
缓存降级与容错处理
当缓存服务完全不可用的时候,需要有降级策略,避免所有请求都打到数据库。可以在Laravel中配置缓存的容错逻辑,当缓存连接失败的时候,直接返回默认值或者空数据,同时记录日志,后续再排查缓存服务的问题。
可以通过自定义缓存驱动或者封装缓存调用的方式实现容错,示例如下:
<?php
namespace AppSupport;
use IlluminateSupportFacadesCache;
use IlluminateSupportFacadesLog;
class CacheHelper
{
/**
* 带容错的缓存获取方法
* @param string $key 缓存键
* @param mixed $default 缓存不存在或者失败时的默认值
* @return mixed
*/
public static function getWithFallback($key, $default = null)
{
try {
$data = Cache::get($key);
if (!is_null($data)) {
return $data;
}
return $default;
} catch (Exception $e) {
// 缓存服务异常,记录日志,返回默认值
Log::error('缓存获取失败:' . $e->getMessage(), ['key' => $key]);
return $default;
}
}
/**
* 带容错的缓存存储方法
* @param string $key 缓存键
* @param mixed $value 缓存值
* @param int $ttl 过期时间(秒)
* @return bool
*/
public static function putWithFallback($key, $value, $ttl = 3600)
{
try {
return Cache::put($key, $value, $ttl);
} catch (Exception $e) {
Log::error('缓存存储失败:' . $e->getMessage(), ['key' => $key]);
return false;
}
}
}
在业务代码中调用这个辅助类来获取和存储缓存,就可以在缓存服务异常的时候自动降级,避免请求直接穿透到数据库。
多级缓存架构
除了单级缓存,还可以使用多级缓存的架构,比如本地缓存加分布式缓存的组合。本地缓存可以使用Laravel的array缓存驱动,分布式缓存使用Redis。请求先查本地缓存,再查分布式缓存,最后才查数据库。即使分布式缓存出现问题,本地缓存还可以扛住一部分请求,减少雪崩的影响。
多级缓存的简单实现示例如下:
<?php
namespace AppServices;
use IlluminateSupportFacadesCache;
class ConfigService
{
/**
* 获取系统配置,多级缓存
*/
public function getConfig($key)
{
// 先查本地缓存(array驱动,请求生命周期内有效)
$localKey = 'local_config_' . $key;
$localCache = Cache::store('array')->get($localKey);
if (!is_null($localCache)) {
return $localCache;
}
// 再查分布式缓存(Redis)
$redisKey = 'config_' . $key;
$redisCache = Cache::store('redis')->get($redisKey);
if (!is_null($redisCache)) {
// 存入本地缓存,避免重复查分布式缓存
Cache::store('array')->put($localKey, $redisCache, 60);
return $redisCache;
}
// 缓存都不存在,查数据库
$config = AppModelsConfig::where('key', $key)->first();
if ($config) {
$value = $config->value;
// 存入分布式缓存,设置随机过期时间
$ttl = 86400 + rand(0, 3600);
Cache::store('redis')->put($redisKey, $value, $ttl);
// 存入本地缓存
Cache::store('array')->put($localKey, $value, 60);
return $value;
}
return null;
}
}
通过以上几种方法的组合使用,可以有效避免Laravel项目中出现缓存雪崩的问题,提升系统的稳定性和可用性。在实际项目中,可以根据业务场景选择合适的方案,或者将多种方案结合使用,达到更好的防护效果。