Laravel缓存击穿用互斥锁吗

wen PHP项目 23

本文目录导读:

Laravel缓存击穿用互斥锁吗

  1. 什么是缓存击穿
  2. 使用互斥锁解决缓存击穿
  3. 更优雅的封装
  4. 其他解决方案对比
  5. 生产环境建议
  6. 完整示例:商品详情页

是的,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、本地缓存、异步更新等多种策略

互斥锁是解决缓存击穿最直接有效的方式,但要处理好锁的超时和释放,避免死锁和性能问题。

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