Лучший способ получить блокировку в php

Я пытаюсь обновить переменную в APC, и многие процессы пытаются это сделать.

APC не обеспечивает функциональность блокировки, поэтому я рассматриваю возможность использования других механизмов... что я нашел до сих пор - это mysql GET_LOCK() и php's flock(). Что-нибудь еще стоит рассмотреть?

Обновление: я нашел sem_acquire, но это, кажется, блокировка блокировки.

11 ответов

/*
CLASS ExclusiveLock
Description
==================================================================
This is a pseudo implementation of mutex since php does not have
any thread synchronization objects
This class uses flock() as a base to provide locking functionality.
Lock will be released in following cases
1 - user calls unlock
2 - when this lock object gets deleted
3 - when request or script ends
==================================================================
Usage:

//get the lock
$lock = new ExclusiveLock( "mylock" );

//lock
if( $lock->lock( ) == FALSE )
    error("Locking failed");
//--
//Do your work here
//--

//unlock
$lock->unlock();
===================================================================
*/
class ExclusiveLock
{
    protected $key   = null;  //user given value
    protected $file  = null;  //resource to lock
    protected $own   = FALSE; //have we locked resource

    function __construct( $key ) 
    {
        $this->key = $key;
        //create a new resource or get exisitng with same key
        $this->file = fopen("$key.lockfile", 'w+');
    }


    function __destruct() 
    {
        if( $this->own == TRUE )
            $this->unlock( );
    }


    function lock( ) 
    {
        if( !flock($this->file, LOCK_EX | LOCK_NB)) 
        { //failed
            $key = $this->key;
            error_log("ExclusiveLock::acquire_lock FAILED to acquire lock [$key]");
            return FALSE;
        }
        ftruncate($this->file, 0); // truncate file
        //write something to just help debugging
        fwrite( $this->file, "Locked\n");
        fflush( $this->file );

        $this->own = TRUE;
        return TRUE; // success
    }


    function unlock( ) 
    {
        $key = $this->key;
        if( $this->own == TRUE ) 
        {
            if( !flock($this->file, LOCK_UN) )
            { //failed
                error_log("ExclusiveLock::lock FAILED to release lock [$key]");
                return FALSE;
            }
            ftruncate($this->file, 0); // truncate file
            //write something to just help debugging
            fwrite( $this->file, "Unlocked\n");
            fflush( $this->file );
            $this->own = FALSE;
        }
        else
        {
            error_log("ExclusiveLock::unlock called on [$key] but its not acquired by caller");
        }
        return TRUE; // success
    }
};

Вы можете использовать функцию apc_add, чтобы достичь этого, не прибегая к файловым системам или MySQL. apc_add только успешно, когда переменная еще не сохранена; Таким образом, обеспечивается механизм блокировки. TTL может использоваться, чтобы гарантировать, что поддельные блокировщики не будут продолжать удерживать блокировку вечно.

Причина apc_add Это правильное решение, потому что оно позволяет избежать состояния гонки, которое могло бы существовать между проверкой блокировки и установкой ее "заблокировано вами". поскольку apc_add устанавливает значение только в том случае, если оно еще не установлено ( "добавляет" его в кэш), оно гарантирует, что блокировка не может быть получена двумя вызовами одновременно, независимо от их близости во времени. Ни одно решение, которое не проверяет и не устанавливает блокировку одновременно, не будет страдать от этого состояния гонки; одна атомная операция необходима для успешной блокировки без состояния гонки.

Поскольку блокировки APC будут существовать только в контексте выполнения php, это, вероятно, не лучшее решение для общей блокировки, так как она не поддерживает блокировки между хостами. Memcache также предоставляет функцию атомарного добавления и, таким образом, также может использоваться с этим методом, который является одним из методов блокировки между хостами. Redis также поддерживает атомарные функции "SETNX" и TTL и является очень распространенным методом блокировки и синхронизации между хостами. Однако ОП запрашивает решение для APC, в частности.

Если цель блокировки состоит в том, чтобы не допустить попытки нескольких процессов заполнить пустой ключ кеша, почему вы не хотите иметь блокирующую блокировку?


  $value = apc_fetch($KEY);

  if ($value === FALSE) {
      shm_acquire($SEMAPHORE);

      $recheck_value = apc_fetch($KEY);
      if ($recheck_value !== FALSE) {
        $new_value = expensive_operation();
        apc_store($KEY, $new_value);
        $value = $new_value;
      } else {
        $value = $recheck_value;
      }

      shm_release($SEMAPHORE);
   }

Если кеш хороший, просто катитесь с ним. Если в кеше ничего нет, вы получаете блокировку. Когда у вас есть блокировка, вам нужно будет дважды проверить кеш, чтобы убедиться, что, пока вы ожидали получить блокировку, кеш не был снова заполнен. Если кэш был снова заполнен, используйте это значение и снимите блокировку, в противном случае вы выполните вычисления, заполните кеш и снимите блокировку.

Если вы не возражаете против блокировки вашей файловой системы, вы можете использовать fopen() с режимом 'x'. Вот пример:

$f = fopen("lockFile.txt", 'x');
if($f) {
    $me = getmypid();
    $now = date('Y-m-d H:i:s');
    fwrite($f, "Locked by $me at $now\n");
    fclose($f);
    doStuffInLock();
    unlink("lockFile.txt"); // unlock        
}
else {
    echo "File is locked: " . get_file_contents("lockFile.txt");
    exit;
}

Смотрите www.php.net/fopen

На самом деле, проверьте, будет ли это работать лучше, чем предложение Питера.

http://us2.php.net/flock

используйте эксклюзивную блокировку и, если вам удобно, поместите все, что пыталось заблокировать файл, в течение 2-3 секунд. Если все сделано правильно, ваш сайт будет зависать от заблокированного ресурса, но не будет скриптов, пытающихся кешировать то же самое.

Я понимаю, что это год, но я просто наткнулся на этот вопрос, проводя некоторые исследования по блокировке в PHP.

Мне приходит в голову, что решение может быть возможно с использованием самого APC. Назовите меня сумасшедшим, но это может быть осуществимым подходом:

function acquire_lock($key, $expire=60) {
    if (is_locked($key)) {
        return null;
    }
    return apc_store($key, true, $expire);
}

function release_lock($key) {
    if (!is_locked($key)) {
        return null;
    }
    return apc_delete($key);
}

function is_locked($key) {
    return apc_fetch($key);
}

// example use
if (acquire_lock("foo")) {
    do_something_that_requires_a_lock();
    release_lock("foo");
}

На практике я мог бы добавить другую функцию, чтобы сгенерировать ключ для использования здесь, просто чтобы предотвратить столкновение с существующим ключом APC, например:

function key_for_lock($str) {
    return md5($str."locked");
}

$expire Параметр - хорошая функция APC, поскольку он предотвращает удержание блокировки навсегда, если ваш скрипт умирает или что-то в этом роде.

Надеюсь, этот ответ будет полезен для всех, кто спотыкается здесь год спустя.

БТР теперь считается необслуживаемым и мертвым. Его преемник APCu предлагает блокировку через apcu_entry, Но имейте в виду, что он также запрещает одновременное выполнение любых других функций APCu. В зависимости от вашего варианта использования, это может быть хорошо для вас.

Из руководства:

Примечание: когда элемент управления входит apcu_entry() блокировка для кеша приобретается исключительно, она снимается при выходе из управления apcu_entry(): По сути, это превращает тело generator в критическую секцию, не позволяющую двум процессам одновременно выполнять одни и те же пути кода. Кроме того, он запрещает одновременное выполнение любых других функций APCu, поскольку они получат такую ​​же блокировку.

APCu имеет apcu_entry начиная с 5.1.0, теперь с ним можно реализовать механизм блокировки:

      /** get a lock, will wait until the lock is available,
 * make sure handle deadlock yourself :p
 * 
 * useage : $lock = lock('THE_LOCK_KEY', uniqid(), 50);
 * 
 * @param $lock_key : the lock you want to get it
 * @param $lock_value : the unique value to specify lock owner
 * @param $retry_millis : wait befor retry
 * @return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value]
 */
function lock($lock_key, $lock_value, $retry_millis) {
    $got_lock = false;
    while (!$got_lock) {
        $fetched_lock_value = apcu_entry($lock_key, function ($key) use ($lock_value) {
            return $lock_value;
        }, 100);
        $got_lock = ($fetched_lock_value == $lock_value);
        if (!$got_lock) usleep($retry_millis*1000);
    }
    return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value];
}

/** release a lock
 * 
 * usage : unlock($lock);
 * 
 * @param $lock : return value of function lock
 */
function unlock($lock) {
    apcu_delete($lock['lock_key']);
}

Не могу сказать, является ли это лучшим способом справиться с работой, но по крайней мере это удобно.

function WhileLocked($pathname, callable $function, $proj = ' ')
{
    // create a semaphore for a given pathname and optional project id
    $semaphore = sem_get(ftok($pathname, $proj)); // see ftok for details
    sem_acquire($semaphore);
    try {
        // capture result
        $result = call_user_func($function);
    } catch (Exception $e) {
        // release lock and pass on all errors
        sem_release($semaphore);
        throw $e;
    }

    // also release lock if all is good
    sem_release($semaphore);
    return $result;
}

Использование так же просто, как это.

$result = WhileLocked(__FILE__, function () use ($that) {
    $this->doSomethingNonsimultaneously($that->getFoo());
});

Третий необязательный аргумент может пригодиться, если вы используете эту функцию более одного раза для каждого файла.

Наконец, что не менее важно, несложно изменить эту функцию (сохраняя свою подпись), чтобы позднее использовать какой-либо другой вид механизма блокировки, например, если вам случится работать с несколькими серверами.

У EAccelerator есть методы для этого; eaccelerator_lock а также eaccelerator_unlock,

На самом деле я обнаружил, что мне вообще не нужна блокировка... учитывая, что я пытаюсь создать карту всех ассоциаций class => path для автозагрузки, не имеет значения, один процесс перезаписывает то, что нашел другой (очень маловероятно, если его правильно закодировать), потому что в любом случае данные попадут туда. Таким образом, решение оказалось "без замков".

Другие вопросы по тегам