2011-08-06 11 views
11

La página de documentación de PHP para flock() indica que no es seguro usarla bajo IIS. Si no puedo confiar en flock bajo ninguna circunstancia, ¿hay alguna otra forma en que pueda lograr lo mismo de forma segura?PHP flock() alternativa

+0

'flock()' también es un inconveniente si necesita evitar leer archivos de 0-length. Esto se debe a que 'flock()' solo se puede llamar _después_ de que se cree un archivo, es imposible crear un nuevo archivo y escribir en él de forma atómica. – rustyx

+0

Además, he leído que el bloqueo obligatorio de archivos está en desuso en Linux, por lo que el rebaño en realidad no es ideal (también es un poco difícil de configurar) – Antony

Respuesta

7

No hay otra alternativa disponible para lograr de forma segura el mismo en todas las circunstancias imaginarias posibles. Eso es por diseño de sistemas informáticos y the job is not trivial for cross-platform code.

Si necesita hacer un uso seguro de flock(), documente los requisitos para su aplicación en su lugar.

Como alternativa, puede crear su propio mecanismo de bloqueo, sin embargo, debe asegurarse de que sea atómico. Esto significa que debe probar el bloqueo y, si no existe, establecer el bloqueo mientras necesita asegurarse de que nada más pueda adquirir el bloqueo intermedio.

Esto se puede hacer creando un archivo de bloqueo que represente el bloqueo, pero solo si no existe. Desafortunadamente, PHP no ofrece esa función para crear un archivo de esa manera.

O bien puede crear un directorio con mkdir() y trabajar con el resultado porque devolverá true cuando se creó el directorio y false si ya existió.

+0

Eso es perfecto. Había pensado en los archivos de bloqueo, pero hay problemas para implementarlos en PHP. También pensé en usar una base de datos con bloqueo de filas para establecer y desarmar un indicador de "bloqueado", pero esto es lento y no robusto tampoco.Sin embargo, ¡no pensé en usar un directorio en lugar de un archivo de bloqueo! ¡Gracias! – Matty

+0

Pero esto solo funciona * si * 'mkdir()' devuelve los valores especificados. No sé si ese * es * el caso para todas las combinaciones plataforma/sistema de archivos. – hakre

+0

'mkdir()' se comporta correctamente, al menos en Linux. Voy a probar esto en un servidor de Windows. Si no funciona como se esperaba, volveré para actualizar esto. – Matty

1

Mi propuesta es usar mkdir() en lugar de flock(). Este es un ejemplo del mundo real para la lectura/escritura de las memorias caché que muestra las diferencias:

$data = false; 
$cache_file = 'cache/first_last123.inc'; 
$lock_dir = 'cache/first_last123_lock'; 
// read data from cache if no writing process is running 
if (!file_exists($lock_dir)) { 
    // we suppress error messages as the cache file exists in 99,999% of all requests 
    $data = @include $cache_file; 
} 
// cache file not found 
if ($data === false) { 
    // get data from database 
    $data = mysqli_fetch_assoc(mysqli_query($link, "SELECT first, last FROM users WHERE id = 123")); 
    // write data to cache if no writing process is running (race condition safe) 
    // we suppress E_WARNING of mkdir() because it is possible in 0,001% of all requests that the dir already exists after calling file_exists() 
    if (!file_exists($lock_dir) && @mkdir($lock_dir)) { 
     file_put_contents($cache_file, '<?php return ' . var_export($data, true) . '; ?' . '>')) { 
     // remove lock 
     rmdir($lock_dir); 
    } 
} 

Ahora, tratamos de lograr lo mismo con flock():

$data = false; 
$cache_file = 'cache/first_last123.inc'; 
// we suppress error messages as the cache file exists in 99,999% of all requests 
$fp = @fopen($cache_file, "r"); 
// read data from cache if no writing process is running 
if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) { 
    // we suppress error messages as the cache file exists in 99,999% of all requests 
    $data = @include $cache_file; 
    flock($fp, LOCK_UN); 
} 
// cache file not found 
if (!is_array($data)) { 
    // get data from database 
    $data = mysqli_fetch_assoc(mysqli_query($link, "SELECT first, last FROM users WHERE id = 123")); 
    // write data to cache if no writing process is running (race condition safe) 
    $fp = fopen($cache_file, "c"); 
    if (flock($fp, LOCK_EX | LOCK_NB)) { 
     ftruncate($fp, 0); 
     fwrite($fp, '<?php return ' . var_export($data, true) . '; ?' . '>'); 
     flock($fp, LOCK_UN); 
    } 
} 

La parte importante es LOCK_NB para evitar el bloqueo de todos peticiones consecutivas:

también es posible añadir LOCK_NB como una máscara de bits a una de las operaciones anteriores si no desea que flock() para bloquear WH bloqueo de ile.

¡Sin él, el código produciría un gran cuello de botella!

Una parte importante adicional es if (!is_array($data)) {.Esto se debe a que los datos de $ podrían contener:

  1. array() como resultado de la consulta db
  2. false de la no include
  3. o una cadena vacía (race condition)

La condición de carrera sucede si el primer visitante ejecuta esta línea:

$fp = fopen($cache_file, "c"); 

y otro visitante ejecuta esta línea de un milisegundo más tarde:

if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) { 

Esto significa que el primer visitante crea el archivo vacío, pero el segundo visitante crea la cerradura y así include devuelve una cadena vacía.

Así que vio muchas trampas que se pueden evitar mediante el uso de mkdir() y su 7x más rápido, también:

$filename = 'index.html'; 
$loops = 10000; 
$start = microtime(true); 
for ($i = 0; $i < $loops; $i++) { 
    file_exists($filename); 
} 
echo __LINE__ . ': ' . round(microtime(true) - $start, 5) . PHP_EOL; 
$start = microtime(true); 
for ($i = 0; $i < $loops; $i++) { 
    $fp = @fopen($filename, "r"); 
    flock($fp, LOCK_EX | LOCK_NB); 
} 
echo __LINE__ . ': ' . round(microtime(true) - $start, 5) . PHP_EOL; 

resultado:

file_exists: 0.00949 
fopen/flock: 0.06401 

P. S. como puedes ver, uso file_exists() delante de mkdir(). Esto se debe a que my tests (alemán) dio lugar a cuellos de botella con mkdir() solo.

+1

"Si bloquea un archivo y su script se detiene al escribir a un archivo se libera el bloqueo ". - si está utilizando bloqueos para controlar qué acceso tienen los procesos en ejecución a un recurso compartido, esta es una característica muy deseable. No preocuparse por las cerraduras que cuelgan después de que un proceso ha muerto, facilita el procesamiento. Como de costumbre, mucho depende de lo que finalmente estás tratando de lograr. – Jason

+1

Hay tanto uso de [the @ operator] (http://php.net/manual/en/language.operators.errorcontrol.php) allá arriba, esto podría explotar de muchas maneras y nunca lo harías saber lo que sucedió –

+0

@JosipRodin Reescribí mi respuesta. Todavía uso el operador @ pero agregué explicaciones. – mgutt

2

Puede implementar un patrón de desbloqueo de archivo alrededor de sus operaciones de lectura/escritura basado en mkdir, ya que es atómico y bastante rápido. He puesto a prueba esto y, a diferencia de mgutt, no encontré un cuello de botella. Sin embargo, debes ocuparte de las situaciones de estancamiento, que es probablemente lo que experimentó mgutt. Un bloqueo inactivo ocurre cuando dos intentos de bloqueo siguen esperando el uno al otro. Puede remediarse mediante un intervalo aleatorio en los intentos de bloqueo. De este modo:

// call this always before reading or writing to your filepath in concurrent situations 
function lockFile($filepath){ 
    clearstatcache(); 
    $lockname=$filepath.".lock"; 
    // if the lock already exists, get its age: 
    [email protected]($lockname); 
    // attempt to lock, this is the really important atomic action: 
    while ([email protected]($lockname)){ 
     if ($life) 
      if ((time()-$life)>120){ 
       //release old locks 
       rmdir($lockname); 
       $life=false; 
     } 
     usleep(rand(50000,200000));//wait random time before trying again 
    } 
} 

luego trabajar en su archivo en ruta de archivo y cuando haya terminado, lo llaman:

function unlockFile($filepath){ 
    $unlockname= $filepath.".lock"; 
    return @rmdir($unlockname); 
} 

que he elegido para eliminar viejos bloqueos, así después de que el tiempo máximo de ejecución en PHP caso de que una secuencia de comandos salga antes de que se haya desbloqueado. Una forma mejor sería eliminar los bloqueos siempre que el script falle. Hay una buena manera de hacerlo, pero lo he olvidado.

0

Agradezco que esta pregunta tenga algunos años, pero sentí que un ejemplo de trabajo/reemplazo para el rebaño podría valer la pena. Basé esto en las otras respuestas, pero para alguien que simplemente busca reemplazar la funcionalidad de bandada (en lugar de escribir un archivo al mismo tiempo (aunque esto sí refleja el ejemplo de manada manual de PHP)) Creo que lo siguiente será suficiente

function my_flock ($path,$release = false){ 
    if ($release){ 
     @rmdir($path); 
    } else { 
     return !file_exists($path) && @mkdir($path); 
    } 
} 
Cuestiones relacionadas