在某些情况下文件被覆盖

时间:2019-02-14 10:19:46

标签: php concurrency

我正在使用一个接收推送通知并通过使用XSL模板对其进行解析并创建(如果不存在)或将XML解析的数据附加到文件中来处理它们的系统。 有时它几乎同时接收到多个通知,因此第一个通知创建了文件,但是第二个通知又创建了该文件,因为在那一刻我认为文件正在写入但尚不存在于文件系统中。因此,结果是第一个xml数据因第二次调用脚本而被覆盖而丢失。

我认为主要问题是在尝试使用scan_dir来检查文件是否已存在时。第一次调用找不到该文件(正确),但是第二次调用找不到该文件,因为第一次调用正在写入该文件。我没有办法确定文件名,所以我必须应用正则表达式模式,因为不知道文件是何时创建的。最初,我使用glob具有相同的结果,因此更改为scan_dir,因为我认为scan_dir更快。

/**
 * Main method
 * @return string XML resulting
 */
public function run()
{
    $xml = simplexml_load_string($this->xml);
    $xsl = new \DOMDocument();
    $xsl->load(self::XSLPATH);

    // Transformer config
    $proc = new \XSLTProcessor;
    $proc->registerPHPFunctions();
    $proc->importStyleSheet($xsl);

    $xmlResult = $proc->transformToXML($xml);

    //  Obtain path to destination folder
    $path = self::PATH."/{$this->folder}/";
    //  Get the file with pattern
    $file_list = glob($path."fileoutput*.xml");


    //  If there exists files matching the pattern, get the first one
    if (sizeof($file_list) > 0) {
        $this->append($xmlResult, $file_list[0]);
    } else {
        // ERROR! concurrent calls end here beceause file is not 
        // in filesystem so scan_dir can't detect it!
        $filename = "fileoutput_".date("d-m-Y_H-i-s").".xml";
        $this->writeFile(
            __DIR__."/../../../output/{$this->folder}/{$filename}",
            $xmlResult
        );
    }

    //  return the xml string
    return $xmlResult;
}

/**
 * This method uses flock to gain exclusive acces to resource.
 * 
 * @param string $filepath file path
 * @param string $data dat ato be written
 * @return void
 */
private function writeFile($filepath, $data)
{
    $fh = fopen($filepath, "w");
    $tries = 5;

    while ($tries > 0) {
        $locked = flock($fh, LOCK_EX);
        if (! $locked) {
            sleep(5);
            $tries--;
        } else {
            $tries = 0;
        }
    }

    if ($locked) {
        fwrite($fh, $data);
        flock($fh, LOCK_UN);
    }

    fclose($fh);
}

/**
 * Append xml data to existing xml
 * @param $xml string xml to append
 * @param $file string file where xml will be append
 */
private function append($xml, $filename)
{
    $xmlFromFile = simplexml_load_file($filename);
    $xmlToAppend = simplexml_load_string($xml);

    $nodeToAppend = $xmlToAppend->reserva;
    $this->sxml_append($xmlFromFile, $nodeToAppend);

    $this->writeFile($filename, $xmlFromFile->asXML());

}

/**
 * This method adds a childnode to xml with deep copy
 * @param $to SimpleXMLElement xml where childnode is copied
 * @param $from SimpleXMLElement xml childnode to copy to
 * @return void
 */
private function sxml_append(\SimpleXMLElement $to, \SimpleXMLElement $from)
{
    $toDom = dom_import_simplexml($to);
    $fromDom = dom_import_simplexml($from);

    $toDom->formatOutput = true;
    //$toDom->preserveWhiteSpace = false;

    $toDom->appendChild($toDom->ownerDocument->createTextNode("\n"));   
    $toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true));
}

我正在尝试处理所有通知数据,即使它们几乎同时到达我的脚本。如果我几乎同时收到2条通知,则需要所有数据而不会覆盖它们。

1 个答案:

答案 0 :(得分:1)

按照RiggsFolly的建议,如果我创建一个 lock.dat 文件并尝试对其进行锁定,则可以编写或以安全的方式获取输出文件,避免数据被覆盖。

我附上了修改后的代码。尝试使用jMeter,成功并发5次并发调用PHP脚本!

/**
 * Main method
 * @return string XML resulting
 */
public function run()
{
    $xml = simplexml_load_string($this->xml);
    $xsl = new \DOMDocument();
    $xsl->load(self::XSLPATH);

    // Transformer config
    $proc = new \XSLTProcessor;
    $proc->registerPHPFunctions();
    $proc->importStyleSheet($xsl);

    $xmlResult = $proc->transformToXML($xml);

    //  Obtain path to destination folder
    $path = self::PATH."/{$this->folder}/";
    //  Get the file with pattern
    $file_list = glob($path."fileoutput*.xml");

    $lockfile = fopen("lock.dat", "w");

    //  check if get lock for $lockfile
    if($this->getLock($lockfile)) {
        $file_list = glob($path."fileoutput*.xml");

        //  If there exist files matching the pattern, get the first one
        if (sizeof($file_list) > 0) {
            $this->append($xmlResult, $file_list[0]);
        } else {
            //NICE! now it is sure we create file only when got lock in $lockfile,
            //so it won't be overwriten
            $filename = "fileoutput_".date("d-m-Y_H-i-s").".xml";
            $this->writeFile(__DIR__."/../../../output/{$this->folder}/{$filename}", $xmlResult);
        }

    }

    //  return the xml string
    return $xmlResult;
}

/**
* this method tries to get lock on a file
* @param resource $file The lock file
*
* @return boolean true if we get lock, false if not
*/
private function getLock($file)
    {
        $fh = $file;
        $tries = 5;

        while ($tries > 0) {
            $locked = flock($fh, LOCK_EX);

            if (! $locked) {
                sleep(5);
                $tries--;
            } else {
                $tries = 0;
            }
        }

        return $locked;
    }

/**
 * This method uses flock to gain exclusive acces to resource.
 * 
 * @param string $filepath file path
 * @param string $data dat ato be written
 * @return void
 */
private function writeFile($filepath, $data)
{
    $fh = fopen($filepath, "w");
    $tries = 5;

    while ($tries > 0) {
        $locked = flock($fh, LOCK_EX);
        if (! $locked) {
            sleep(5);
            $tries--;
        } else {
            $tries = 0;
        }
    }

    if ($locked) {
        fwrite($fh, $data);
        flock($fh, LOCK_UN);
    }

    fclose($fh);
}

/**
 * Append xml data to existing xml
 * @param $xml string xml to append
 * @param $file string file where xml will be append
 */
private function append($xml, $filename)
{
    $xmlFromFile = simplexml_load_file($filename);
    $xmlToAppend = simplexml_load_string($xml);

    $nodeToAppend = $xmlToAppend->reserva;
    $this->sxml_append($xmlFromFile, $nodeToAppend);

    $this->writeFile($filename, $xmlFromFile->asXML());

}

/**
 * This method adds a childnode to xml with deep copy
 * @param $to SimpleXMLElement xml where childnode is copied
 * @param $from SimpleXMLElement xml childnode to copy to
 * @return void
 */
private function sxml_append(\SimpleXMLElement $to, \SimpleXMLElement $from)
{
    $toDom = dom_import_simplexml($to);
    $fromDom = dom_import_simplexml($from);

    $toDom->formatOutput = true;
    //$toDom->preserveWhiteSpace = false;

    $toDom->appendChild($toDom->ownerDocument->createTextNode("\n"));   
    $toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true));
}