在PHP中,如何实现互斥锁,避免同时大量请求查询同一个数据导致缓存失效(被击穿)

很多技术确实也只有用到了才能理解。

之前听到缓存失效的三种情况,缓存雪崩、缓存穿透、缓存击穿是很难理解的,因为那时候用的缓存确实比较少,没有那么多问题,也就理解不了这些理论。

最近用了一段时间的缓存,再看这个理论也就熟悉很多了。

首先说一下今天遇到的这个问题:

在 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 扩展还是文件锁,都需要确保在临界区内操作完共享资源后及时释放锁,以避免死锁等问题。

总结

文件锁利用操作系统提供的文件锁定机制,通过文件描述符和原子性操作,确保同一时间只有一个进程可以获得排他性访问,从而实现互斥锁的功能。

这种机制简单且有效,如果不想增加扩展,可以考虑使用这种方法。

相关推荐
阿丰资源1 天前
基于SpringBoot的在线视频教育平台的设计与实现(附源码+数据库+文档,一键运行)
数据库·spring boot·后端
IT_陈寒1 天前
我竟然被JavaScript的隐式类型转换坑了三天!
前端·人工智能·后端
Reart1 天前
从0解构tinyWeb项目--(Day:9)
后端·架构·github
小码哥_常1 天前
Java后端定时任务“三剑客”大比拼,选对不选贵!
后端
oldking呐呐1 天前
MySQL从入门到入土 -- 2.数据库基础
后端·mysql
用户860821135651 天前
从JVM到Spring Boot:一文搞懂胖Jar中的类加载机制
后端
小兵张健1 天前
30天减20斤挑战:少一斤发100红包(2)
后端·程序员·全栈
汤姆Tom1 天前
从 0 到 1 开发项目?你是否也是这样开始?先有再优化一步一步带你了解架构设计
前端·后端·架构
muskk2 天前
一个文件,9万星:Karpathy 用 4 条规则治好了 AI 写代码的"坏毛病"
前端·后端
xlecho2 天前
从单一语言到全域全栈,AI凭全能实力,淘汰旧时代语言工程师
人工智能·后端·开源