很多技术确实也只有用到了才能理解。
之前听到缓存失效的三种情况,缓存雪崩、缓存穿透、缓存击穿是很难理解的,因为那时候用的缓存确实比较少,没有那么多问题,也就理解不了这些理论。
最近用了一段时间的缓存,再看这个理论也就熟悉很多了。
首先说一下今天遇到的这个问题:
在 PHP 中开发接口中,需要为客户端提供三个接口,以显示商品相关的信息。
而这三个接口的数据,都来自于同一个外部接口。
为了避免三个后端接口调用三次外部接口数据,于是我就用了缓存,根据商品ID,相同的商品第一次调用后就缓存起来,后面其它的接口就可以直接用缓存数据,而无需重复调用外部接口。
听起来很合理对吧,以下是第一次的代码:
php
public function getGoodsData($goodsId, $pid = '', $relationId = '')
{
$cacheName = 'goods_' . $goodsId;
$responseArray = Cache::get($cacheName);
if (empty($responseArray)) {
$responseArray = $this->getGoodsDataFromApi($goodsId, $pid, $relationId);
Cache::set($cacheName, $responseArray, 7200);
}
return $responseArray;
}
但查看日志就发现不对劲了,每次都还是发了三次外部请求,缓存压根就没用。
再一想也就理解了,因为这三个接口,客户端是同时请求的,完全没有串行啥的,也就意味着这个方法是同时被调用的,这个时候缓存里面根本就没有数据,所以就要发三次请求,缓存完全失效了。
这个时候互斥锁就可以发挥作用了。
互斥锁
互斥锁(Mutex,Mutual Exclusion Lock)是一种用于控制对共享资源的访问的同步机制。它确保在任何给定的时刻,只有一个线程或进程可以访问共享资源,而其他线程或进程必须等待。
在 PHP 中实现互斥锁可以使用 PHP 提供的扩展或者基于文件、数据库等方式来实现。
本期主要利用文件锁来实现,也就是方法二,不需要安装任何额外的扩展即可实现,比较简单。
如果愿意的话,也可以使用 PHP 扩展 Mutex 来实现,可以看方法一。
方法一、使用 PHP 扩展 Mutex
PHP 提供了一个名为 Mutex
的扩展来实现互斥锁,需要单独安装扩展。
可以使用 Mutex
扩展来确保在多个进程中对共享资源的访问是安全的。
使用示例:
php
<?php
$mutex = Mutex::create();
if (Mutex::lock($mutex)) {
// 这里是临界区,对共享资源进行操作
// 例如:写入文件、访问数据库等操作
Mutex::unlock($mutex);
} else {
// 无法获取锁,处理失败逻辑
}
Mutex::destroy($mutex);
?>
方法二、使用文件锁
文件锁(file lock)实现互斥锁的原理主要利用了操作系统对文件锁定机制的支持。具体来说,它依赖于文件系统提供的文件锁定功能,确保同一时间只有一个进程可以对文件进行排他性访问。
在 PHP 中,可以使用 flock()
函数来实现文件锁。flock()
函数提供了一种简单的接口来使用操作系统的文件锁功能。我们首先尝试获取一个文件锁。如果能够获取到锁,则在临界区内进行操作:检查缓存是否存在,如果不存在则从 API 获取数据并缓存;然后释放锁。如果无法获取到锁,则等待一段时间后再次尝试获取锁。
通过这种方式,相同的 $goodsId
在同时请求时只会有一个请求进入临界区进行 API 访问,其他请求会等待该请求完成后再继续执行。
可以看下面优化后的代码:
php
public function getGoodsData($goodsId, $pid = '', $relationId = '')
{
$cacheName = 'goods_' . $goodsId;
// 尝试从缓存获取数据
$responseArray = Cache::get($cacheName);
// 如果缓存不存在,则尝试获取文件锁
if (empty($responseArray)) {
$lockFile = root_path() . "/runtime/temp/goods_lock_" . $this->safeFileName($goodsId) . ".lock";
$fp = fopen($lockFile, "w+");
// 尝试获取文件锁
if (flock($fp, LOCK_EX)) {
// 再次检查缓存,因为获取文件锁后可能其他请求已经获取了缓存数据
$responseArray = Cache::get($cacheName);
if (empty($responseArray)) {
// 缓存仍然不存在,从 API 获取数据并缓存
$responseArray = $this->getGoodsDataFromApi($goodsId, $pid, $relationId);
Cache::set($cacheName, $responseArray, 7200);
}
// 释放锁
flock($fp, LOCK_UN);
// 临界区操作完成后,删除锁文件
unlink($lockFile);
} else {
// 无法获取锁,等待一段时间再尝试
usleep(10000); // 等待 10 毫秒
fclose($fp);
// 递归调用自身,等待其他请求完成后再次尝试获取数据
return $this->getGoodsData($goodsId, $pid, $relationId);
}
fclose($fp);
}
return $responseArray;
}
现在通过数据库查看请求日子,就可以看到不会出现缓存失效的情况了,只有一个响应是请求了远程数据:
需要注意的是,无论使用 Mutex
扩展还是文件锁,都需要确保在临界区内操作完共享资源后及时释放锁,以避免死锁等问题。
总结
文件锁利用操作系统提供的文件锁定机制,通过文件描述符和原子性操作,确保同一时间只有一个进程可以获得排他性访问,从而实现互斥锁的功能。
这种机制简单且有效,如果不想增加扩展,可以考虑使用这种方法。