Пользователь
0,0
рейтинг
9 января 2013 в 16:29

Разработка → Как же работают транзакции в Redis

Работаю с Redis относительно недавно и вот возникла необходимость изменения одного ключа несколькими потоками одновременно. Для работы с Redis в php использую клиент Rediska. Еще когда читал мануал по Rediska видел раздел про транзакции, а сегодня пришло время почитать внимательнее.



Не знаю, что тому виною, то ли мое плохое знание английского, то ли мое тугодумство, то ли непонятность документации, но тем не менее ознакомившись с документацией по транзакциям на сайте Rediska и потом на сайте самого Redis я так и не понял блокируется ли, изменяемый внутри транзакции, ключ на запись до выполнения execute() или нет.
Да, в обоих документациях есть описание и примеры «Optimistic locking using check-and-set» когда используется watch, но при этом в начале официальной документации на сайте Redis-а написано:

All the commands in a transaction are serialized and executed sequentially. It can never happen that a request issued by another client is served in the middle of the execution of a Redis transaction. This guarantees that the commands are executed as a single isolated operation.

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

$options = array(
    'namespace' => 'Application_',
    'servers'   => array(
       array('host' => '127.0.0.1', 'port' => 6379, 'db' => 5)
    )
);
 
require_once 'Rediska.php';
 
// Get rediska entity
$rediska = new Rediska($options);
 
for ($i=1; $i<=10000; $i++) {
// Start transaction
$transaction = $rediska->transaction();
 
// Get current value
$value = $rediska->get('test_value');
 
// Increment value
$value++;
 
// Store new value
$transaction->set('test_value', $value);
 
// Execute transaction
$transaction->execute();
}
 
echo $rediska->get('test_value');


Это скрипт запускал в два потока из командной строки, т.е. по идее в итоге должен был получить значение у ключа «test_value» равное 20 тысячам, но в реальности там в среднем выходило около 12 тысяч. Т.е. никакой блокировки нет.

Теперь немного модифицирую цикл добавив в него watch:

for ($i=1; $i<=10000; $i++) {
for ($j=1; $j<=5; $j++) {
// Start transaction
$transaction = $rediska->transaction();
 
// Watch
$transaction->watch('test_value');
 
// Get current value
$value = $rediska->get('test_value');
 
// Increment value
$value++;
 
// Store new value
$transaction->set('test_value', $value);
 
// Execute transaction
try {
$transaction->execute();
} catch (Rediska_Transaction_AbortedException $e) {
continue;
  }
 
  break;
}
}


Т.е. по сути при возникновении эксепшена 5-ть раз пытаемся повторить транзакцию. В итоге с 5-тью повторениями в среднем выходило 17 тысяч, если поднять скажем до 10 то будет выходить в среднем 19 тысяч.
Это конечно частные случаи, на практике вряд ли будет такое количество одновременных изменений и 5-ти повторений в принципе должно хватить, но не в этом дело. Дело в том что по факту Redis не имеет пока механизма (задокументированного) для блокировки ключей, затронутых изменениями.
Хорошо это или плохо решать судить не мне, думаю все зависит от задачи, хотел просто показать как оно есть.
Владимир @vagrand
карма
30,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (13)

  • +5
    Смысл транзакции redis в том, что все команды из неё откладываются и потом атомарно выполняются. Это относится к модифицирующим командам, но не к командам чтения т.к.
    $value = $rediska->get('test_value');

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

    Если бы были блокировки, клиенты бы дрались за них и работали бы медленее. Да еще было бы очень просто попасть в deadlock. Разработчикам надо было бы писать немало кода, эти блокировки реализующего, а также ищущего deadlock'и. В NoSQL от этих вещей как раз и пытаются избавиться.

    Кроме того, такие частые конфликты скорее всего говорят о неправильном дизайне приложения. Часто вероятность конфликта мала и в таком случае рулят оптимистичные блокировки, которые и делает watch.
    • 0
      Вы по моему не поняли цель статьи. Целью было разъяснить как работает на самом деле, т.к. в документации на мой взгляд этот вопрос разъяснен недостаточно четко.
      • +1
        Вот из официальной доки по транзакциям:

        The first try may be the following:
        val = GET mykey
        val = val + 1
        SET mykey $val
        

        This will work reliably only if we have a single client performing the operation in a given time…

        Thanks to WATCH we are able to model the problem very well:
        WATCH mykey
        val = GET mykey
        val = val + 1
        MULTI
        SET mykey $val
        EXEC
        

        Using the above code, if there are race conditions and another client modifies the result of val in the time between our call to WATCH and our call to EXEC, the transaction will fail.

        Таким образом, транзакции — это всего лишь элементы в очереди, чтение в очередь не ставится, т.к. иначе не нужен был бы WATCH. В целом, в доке более понятно, как оно работает и почему, нежели в приведенном исследовании.
  • 0
    Если кому интересно, то вот тут я реализовал транзакционный интерфейс работы с редис через расширение phpredis.
  • 0
    Прочитал документацию и у меня сложилось впечатление, что там достаточно последовательно все описано.

    Там написано что есть две фазы — serialized и executed. И что фаза execute — атомарна. И что можно использовать оптимистическую блокировку (если есть желание) с помощью watch. Мы же помним что такое оптимистическая и пессимистическая блокировка, правильно?

    То есть на мой взгляд из документации достаточно очевидно, что пока редис отвечает queued, никто ничего никому не запрещает менять. А когда exec — то все разом и выполнится. Или разом невыполинтся.

  • 0
    Lua процедура.
  • –1
    Хм, что-то странный у вас пример, в редис для таких вещей и придумали incr и decr. Скорее всего у вас проблема в архитектуре и вы что-то используете не по назначению.
    • 0
      Я в курсе про incr и decr, а пример такой же как и в документации на официальном сайте редиса.

      > For example, imagine we have the need to atomically increment the value of a key by 1 (let's suppose Redis doesn't have INCR).
      • 0
        В документации это просто пример, вот не поленился написал скрипт:

        # -*- coding: utf-8 -*-
        
        import redis
        
        r = redis.StrictRedis(host='localhost', port=6379, db=0)
        
        # Test
        for  i in xrange(1, 10001):
            print r.incr('test-key')
        


        Запуск:

        python test.py &
        python test.py &
        python test.py &
        python test.py &
        


        Результат: 40000. Что не так?
        • 0
          Ну так и у меня это тоже просто пример, на примере которого я показал как на самом деле работают транзакции. Это не реальный пример из практики.
          • –1
            Так а в реальном примере что нужно менять? Я просто не могу придумать для чего это нужно.
            • 0
              Что значит для чего нужно? Если у вас в реальной задаче нужно менять значение одного и того-же ключа в несколько потоков.
  • 0
    > Это скрипт запускал в два потока из командной строки, т.е. по идее в итоге должен был получить значение у ключа «test_value» равное 20 тысячам, но в реальности там в среднем выходило около 12 тысяч. Т.е. никакой блокировки нет.

    Я в таких случаях всегда юзаю sleep() когда надо проверить параллельные транзакции. 10К итераций это немного костыль и в некоторых случаях придется довольно долго ждать)

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