浅谈php文件锁机制
以前的自己没有弄懂文件锁的概念,导致自己犯过一个相关的错误。有一个微信公众号的项目,做过微信公众号开发的应该都知道,调用微信公众号的接口基本都需要access_token。微信有专门的接口来获取access_token,这个access_token每条调用次数是有限制的,另外它的有效时间为2个小时。所以,我们获取access_token后需要自行保存。
我当时的做法是,将它保存在文件里,使用json格式保存。类似这样{"access_token":"a2b2af43b2s4ib", "expire":1587114849}
,实现的伪代码如下:
/**
* 获取access_token
* $file 存放access_token的文件
*/
function getAccessToken ($file)
{
$jsonData = file_get_contents($file);
if (!$jsonData) {
$token = setToken($file);
} else if (json_decode($jsonData, true)['expire'] <= time()){
$token = setToken($file);
} else {
$token = json_decode($jsonData, true)['access_token'];
}
return $token;
}
/**
* 获取access_token,并将其保存在文件中
*/
function setToken ($file)
{
$fp = fopen($file, 'r+');
$tokenJson = ...; // 调用微信接口获取到token
fwrite($fp, json_encode($tokenJson));
return $tokenJson['access_token'];
}
然后,项目运行了一段时间后,出现了问题,但是过了1到2秒再刷新就又正常了。第一次没在意,以为是网络或其他什么问题。但之后又再现时,我就知道肯定是哪里出了问题。然后自己一步步去排查,终于发现的问题所在。
问题原因:当access_token到期了,这个时候,如果有多个请求差不多时间来了,就可能出现两个现象:
- 请求A发现access_token过期了,那么它就需要去获取数据然后保存到文件中,在他将要保存前,B来了,这个时候,他看access_token过期了,他也会去获取数据然后保存。他们两同时向文件写入数据,就很可能导致文件内容被破坏。
- 请求A发现access_token过期了,那么它就需要去获取数据然后保存到文件中,在它保存的过程里,还没保存成功时,这个时候B来了,可能就会读取到不完整的内容。
php文件锁
在PHP中提供了 flock()函数,可以对文件使用锁定机制(锁定或释放文件)。当一个进程在访问文件时加上锁,其他进程要想对该文件进行访问,则必须等到锁定被释放以后。这样就可以避免在并发访问同一个文件时破坏数据。
函数原型如下:
flock ( resource $handle , int $operation [, int &$wouldblock ] ) : bool
handle
:文件系统指针,是典型地由 fopen() 创建的 resource(资源)。- operation
operation 可以是以下值之一:
LOCK_SH取得共享锁定(读取的程序)。
LOCK_EX 取得独占锁定(写入的程序)。
LOCK_UN 释放锁定(无论共享或独占)。
LOCK_NB附加锁定(Windows 上还不支持)。
wouldblock
如果锁定会堵塞的话(EWOULDBLOCK 错误码情况下),可选的第三个参数会被设置为 TRUE。(Windows 上不支持)
demo
demo1.php
<?php
$file = 'data.txt';
$handler = fopen($file, 'a+') or die('文件资源打开失败');
// 取得独占锁
if (flock($handler, LOCK_EX)) {
sleep(5);
flock($handler, LOCK_UN);
} else {
echo '锁定失败';
}
fclose($handler);
demo2.php
<?php
$file = 'data.txt';
$handler = fopen($file, 'a+') or die('文件资源打开失败');
// 取得独占锁
if (flock($handler, LOCK_EX)) {
fwrite($handler, 'sometest string');
flock($handler, LOCK_UN);
} else {
echo '锁定失败';
}
fclose($handler);
先运行demo1.php然后立即运行demo2.php,会发现,因为被demo1.php锁定了文件,demo2.php写入不了新内容,只有等demo1.php释放了锁定,demo2.php才能拿到独占锁,然后才能写入文件。
问题解决
学完这些知识后,就能解决我之前的问题了。改进的伪代码如下:
function getToken ($file)
{
$tokenJson = file_get_contents($file);
if (!$tokenJson) {
$token = loadToken($file);
} else if (json_decode($tokenJson, true)['expire'] <= time()){
$token = loadToken($file);
} else {
$token = json_decode($tokenJson, true)['access_token'];
}
return $token;
}
function loadToken ($file)
{
$fp = fopen($file, 'w');
// 取得独占锁
if (flock($fp, LOCK_EX)) {
$tokenJson = ...; // 调用微信接口获取到token
fwrite($fp, json_encode($tokenJson));
flock($fp, LOCK_UN);
} else {
return false;
}
return $tokenJson['access_token'];
}