本文目录导读:

是的,Laravel 中可以使用互斥锁(Mutex)来解决缓存击穿问题,这通常比简单的 Cache::remember 更加可靠。
什么是缓存击穿
缓存击穿是指一个热点 key 在缓存过期瞬间,大量并发请求同时查询数据库,导致数据库压力剧增。
使用互斥锁解决缓存击穿
核心思路
当缓存过期时,只允许一个线程去重建缓存,其他线程等待或直接返回旧数据。
示例代码
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
public function getHotProduct($id)
{
$cacheKey = "hot_product_{$id}";
$lockKey = "lock:{$cacheKey}";
$ttl = 3600; // 缓存有效期
$lockTimeout = 10; // 锁超时时间(防止死锁)
// 1. 尝试获取缓存
$product = Cache::get($cacheKey);
if ($product !== null) {
return $product;
}
// 2. 尝试获取互斥锁(只有第一个请求能成功)
$lock = Cache::lock($lockKey, $lockTimeout);
if ($lock->get()) {
try {
// 3. 双重检测:在拿到锁后再次检查缓存
$product = Cache::get($cacheKey);
if ($product !== null) {
return $product;
}
// 4. 从数据库加载数据(只有一个线程执行)
$product = Product::find($id);
// 5. 写入缓存
Cache::put($cacheKey, $product, $ttl);
return $product;
} finally {
// 6. 释放锁
$lock->release();
}
}
// 7. 其他线程:等待或直接返回降级数据
// 方案A:等待锁释放后重试
sleep(50); // 微秒级等待
return Cache::get($cacheKey); // 此时应该已有缓存
// 方案B:直接返回旧数据(如果有)
// return Cache::get($cacheKey) ?? $this->fallbackData();
}
更优雅的封装
使用闭包的方式
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Cache\LockTimeoutException;
class CacheService
{
public function rememberWithLock($key, $ttl, callable $callback, $lockTimeout = 10)
{
// 先尝试获取缓存
if ($cached = Cache::get($key)) {
return $cached;
}
$lock = Cache::lock("lock:{$key}", $lockTimeout);
try {
// 尝试获取锁,超时则抛出异常
$lock->block(5); // 最多等待5秒
// 双重检测
if ($cached = Cache::get($key)) {
return $cached;
}
// 执行回调重建缓存
$value = $callback();
Cache::put($key, $value, $ttl);
return $value;
} catch (LockTimeoutException $e) {
// 获取锁超时,返回旧数据或降级
return Cache::get($key) ?? $callback();
} finally {
optional($lock)->release();
}
}
}
// 使用
$product = app(CacheService::class)->rememberWithLock(
"product_{$id}",
3600,
fn() => Product::find($id)
);
其他解决方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 互斥锁 | 精确控制并发,数据一致性高 | 增加锁开销,可能降低吞吐量 |
| 二级缓存 | 读多写少场景性能好 | 实现复杂,内存占用大 |
| 异步重建 | 不影响用户请求 | 数据可能滞后 |
| 逻辑过期 | 简单高效 | 需要额外字段,可能读到过期数据 |
生产环境建议
使用 Redis 分布式锁
// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
// 确保使用 Redis 驱动,支持分布式锁
设置合理的 TTL
// 热点数据:基础 TTL + 随机偏移,防止雪崩 $baseTtl = 3600; $randomOffset = rand(0, 300); Cache::put($key, $value, $baseTtl + $randomOffset);
熔断降级
try {
return cache()->lock('key', 10)->block(3, function () {
// 重建缓存
});
} catch (LockTimeoutException) {
// 降级返回默认值
return $this->getDefaultProduct();
}
监控告警
$start = microtime(true);
try {
// 业务代码
} finally {
$duration = (microtime(true) - $start) * 1000;
if ($duration > 500) {
Log::warning("Cache rebuild slow: {$duration}ms", ['key' => $key]);
}
}
完整示例:商品详情页
class ProductController
{
public function show($id)
{
$cacheKey = "product:detail:{$id}";
return Cache::remember($cacheKey, 3600, function () use ($id) {
// 使用互斥锁保护数据库
$lock = Cache::lock("product:lock:{$id}", 10);
return $lock->get(function () use ($id) {
// 双重缓存检测
$cached = Cache::get("product:lock:{$id}");
return Product::with(['category', 'skus'])
->where('status', 1)
->findOrFail($id);
});
});
}
}
最佳实践总结:
- 高并发场景:必须使用互斥锁 + 双重检测
- 普通场景:使用
Cache::remember即可 - 极端场景:配合 CDN、本地缓存、异步更新等多种策略
互斥锁是解决缓存击穿最直接有效的方式,但要处理好锁的超时和释放,避免死锁和性能问题。