本文目录导读:

在PHP项目中解决缓存击穿问题,互斥锁是经典且有效的方案,核心思想是:当缓存失效时,只允许一个线程去重建缓存,其他线程等待。
以下是几种常见的互斥锁实现方案,并附有代码示例:
基于文件锁 (flock) 的互斥锁
适用于单机部署,简单但可能影响性能。
<?php
function getDataWithFileLock($key) {
$cacheKey = 'your_prefix:' . $key;
$cache = new Redis(); // 假设使用Redis作为缓存
// 1. 尝试获取缓存
$data = $cache->get($cacheKey);
if ($data !== false) {
return json_decode($data, true);
}
// 2. 获取文件锁(互斥)
$lockKey = '/tmp/lock_' . md5($cacheKey) . '.lock';
$fp = fopen($lockKey, 'w+');
if (flock($fp, LOCK_EX | LOCK_NB)) { // 非阻塞尝试获取锁
// 3. 双重检查:可能等待期间缓存已被重建
$data = $cache->get($cacheKey);
if ($data !== false) {
flock($fp, LOCK_UN);
fclose($fp);
return json_decode($data, true);
}
// 4. 构建数据
$data = buildExpensiveData(); // 例如从数据库查询
// 5. 设置缓存(建议设置过期时间)
$cache->setex($cacheKey, 3600, json_encode($data));
// 6. 释放锁
flock($fp, LOCK_UN);
fclose($fp);
return $data;
} else {
// 7. 无法获取锁,等待后重试
usleep(100000); // 等待100毫秒
return getDataWithFileLock($key); // 递归重试
}
}
// 使用
$result = getDataWithFileLock('user_profile_123');
基于Redis的SETNX互斥锁(推荐)
适用于分布式部署,Redis提供了原子操作。
<?php
class CacheMutex {
private $redis;
private $lockPrefix = 'lock:';
private $lockTimeout = 5; // 锁超时秒数
private $retryDelay = 100000; // 微秒
private $maxRetries = 10;
public function __construct($redis) {
$this->redis = $redis;
}
public function getWithMutex($key, callable $callback, $ttl = 3600) {
$lockKey = $this->lockPrefix . $key;
$data = $this->redis->get($key);
// 缓存命中
if ($data !== false && $data !== null) {
return json_decode($data, true);
}
// 尝试获取锁
$retryCount = 0;
while ($retryCount < $this->maxRetries) {
// SET NX EX 原子操作
$locked = $this->redis->set($lockKey, time() + $this->lockTimeout,
['nx', 'ex' => $this->lockTimeout]);
if ($locked) {
try {
// 双重检查:可能等待期间缓存已被重建
$data = $this->redis->get($key);
if ($data !== false && $data !== null) {
return json_decode($data, true);
}
// 执行数据构建
$result = $callback();
// 设置缓存
if ($result !== null) {
$this->redis->setex($key, $ttl, json_encode($result));
}
return $result;
} finally {
// 释放锁(使用Lua脚本确保原子性)
$this->releaseLock($lockKey);
}
}
// 等待后重试
$retryCount++;
usleep($this->retryDelay);
}
// 达到最大重试次数,降级处理
return $this->fallback($key);
}
private function releaseLock($lockKey) {
// 使用Lua脚本:只有当前进程持有的锁才释放,防止误删
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
$this->redis->eval($script, [$lockKey, time() + $this->lockTimeout], 1);
}
private function fallback($key) {
// 降级策略:返回旧缓存或默认值
$oldData = $this->redis->get($key);
if ($oldData !== false) {
return json_decode($oldData, true);
}
return null; // 或者返回默认数据
}
}
// 使用
$cacheMutex = new CacheMutex($redis);
$data = $cacheMutex->getWithMutex(
'user_profile_123',
function() use ($db) {
// 这里是数据构建逻辑
return $db->query('SELECT * FROM users WHERE id = 123');
},
3600
);
基于Memcached的互斥锁
<?php
function getDataWithMemcachedMutex($key, $memcached, $db) {
$cacheKey = 'data:' . $key;
$lockKey = 'lock:' . $key;
$lockTimeout = 5;
// 1. 尝试获取缓存
$data = $memcached->get($cacheKey);
if ($memcached->getResultCode() === Memcached::RES_SUCCESS) {
return $data;
}
// 2. 尝试获取锁
$locked = $memcached->add($lockKey, '1', $lockTimeout);
if ($locked) {
try {
// 双重检查
$data = $memcached->get($cacheKey);
if ($memcached->getResultCode() === Memcached::RES_SUCCESS) {
return $data;
}
// 3. 构建数据
$data = $db->query("SELECT * FROM table WHERE id = ?", [$key]);
// 4. 设置缓存
$memcached->set($cacheKey, $data, 3600);
return $data;
} finally {
// 5. 释放锁
$memcached->delete($lockKey);
}
} else {
// 6. 未获取到锁,等待后重试
usleep(200000); // 200ms
return getDataWithMemcachedMutex($key, $memcached, $db); // 递归重试
}
}
更优雅的实现:使用 Redis 的分布式锁管理
<?php
use Luracast\Config\Config;
use Illuminate\Support\Facades\Cache;
class MutexManager {
private $redis;
public function __construct() {
$this->redis = Cache::store('redis')->getRedis();
}
public function getWithMutex($key, callable $callback, $ttl = 60) {
$lockKey = "mutex:$key";
$lockValue = uniqid('', true);
$lockTTL = 10; // 锁持有时间
// 使用 RedLock 算法(功能简化版)
$locked = $this->redis->set(
$lockKey,
$lockValue,
['NX', 'EX' => $lockTTL]
);
if ($locked) {
try {
$data = Cache::get($key);
if ($data !== null) {
return $data;
}
$data = $callback();
Cache::put($key, $data, $ttl);
return $data;
} finally {
// 释放锁,只释放自己的锁
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
$this->redis->eval($script, [$lockKey, $lockValue], 1);
}
}
// 未获取到锁,等待后重试
usleep(100000); // 100ms
return $this->getWithMutex($key, $callback, $ttl);
}
}
最佳实践建议
锁的超时设置
- 设置合理的超时时间:防止持有锁的进程崩溃导致死锁
- 锁时间应远大于数据构建时间:避免锁过期后多个进程同时构建数据
双重检查模式
// 在获取锁后,再次检查缓存
$locked = $this->redis->set($lockKey, '1', ['nx', 'ex' => 5]);
if ($locked) {
// 再次检查缓存(双重检查)
if ($cache->exists($cacheKey)) {
// 其他线程已经重建了缓存,直接返回
return $cache->get($cacheKey);
}
// 开始构建数据...
}
降级策略
// 达到最大重试次数后的降级
public function fallback($key) {
// 1. 返回旧缓存
$oldData = $this->cache->get($key);
if ($oldData) return $oldData;
// 2. 返回默认数据
return ['status' => 'maintenance', 'message' => '数据加载中...'];
// 3. 或者使用本地缓存(例如文件)
$localCache = $this->getLocalCache($key);
}
避免锁竞争优化
// 使用公平锁或梯度等待时间 $waitTime = min(200000, 100000 * ($retryCount + 1)); usleep($waitTime); // 逐渐增加等待时间
注意事项
- 锁的粒度:尽量细粒度,只对热点数据加锁
- 避免永久锁:所有锁必须有超时机制
- 锁竞争监控:如果大量请求等待锁,可能需要考虑:
- 使用更短的锁过期时间
- 使用读写锁(读多写少场景)
- 考虑使用其他方案(如限流、多级缓存)
选择适合你项目架构的方案,分布式系统推荐使用Redis的SETNX,单机部署可以使用文件锁。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播