24 февр. 2015 г.

Lock в файл или в redis?

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

Проще всего создать какой-нибудь lock-файл и проверять его существование. Если есть файл - lock включен.
import os
from contextlib import contextmanager


class LockedException(Exception):
    pass


@contextmanager
def file_lock(lock_file):
    """
    with file_lock('/tmp/file.lock'):
        some code here
    """
    if os.path.exists(lock_file):
        raise LockedException('LockedException')
    else:
        open(lock_file, 'w').write("1")
        try:
            yield
        finally:
            os.remove(lock_file)

Жалко напрягать HDD такими мелкими операциями. Попробуем делать lock в redis:
import redis
from contextlib import contextmanager


r = r = redis.StrictRedis(host='localhost', port=6379, db=0)


class LockedException(Exception):
    pass

@contextmanager
def redis_lock(lock_id):
    """
    with redis_lock('unique_redis_key'):
        some code here
    """
    if r.get(lock_id):
        raise LockedException('LockedException')
    else:
        r.set(lock_id, 1)
        try:
            yield
        finally:
            r.delete(lock_id)

Теперь проверим какой вариант быстрей:
if __name__ == '__main__':
    import time
    from numpy import mean


    count = 10000

    times1 = []
    for i in xrange(0, count):
        start = time.time()
        with file_lock('/tmp/file.lock'):
            pass

        times1.append(float(time.time() - start))

    times2 = []
    for i in xrange(0, count):
        start = time.time()
        with redis_lock('redis_lock'):
            pass

        times2.append(float(time.time() - start))

    print 'file lock:', max(times1), min(times1), mean(times1)
    print 'redis lock:', max(times2), min(times2), mean(times2)

Результаты:

maxminavg
file_lock0.08177495002750.000284910202026 0.000996773791313
redis_lock0.00140619277954 0.000319957733154 0.000346991157532

Lock в redis в 3 раза быстрей.

5 комментариев:

Unknown комментирует...

Чем не устроили семафоры https://docs.python.org/2/library/threading.html#semaphore-objects ?
Думаю что они должны быть быстее :)

Ivan Markeev комментирует...

А почему вообще они должны были меня устраивать? У меня все локи в данной статье перманентные и могут существовать вне контекста исполняемого процесса. Я могу залочить объект в одном скрите, 10 раз его убить, и лок останется. В случае с редисом я дополнительно к тому использую таймауты ключа, чтобы по истечению срока вероятного исполнения скрипта все-таки отпустить залоченный объект и выполнить таки действие над ним.

Unknown комментирует...

Я еще с учебы помню про семафоры и мьютексы, для организации блокировок. А еще я для программ на Delphi делал ограничение на запуск больше одного экземпляра с помощью семафора, т.к. другие варианты, в том числе создание временного файла, отрабатывали дольше и можно было успеть запустить два и больше экземпляра программы, пока запускался первый.
Поэтому я сразу подумал про семафоры.
И мне стало интересно чем они не устроили.
Сам я ими в python не пользовался.
Сейчас глянул, оказывается там нельзя задать идентификатор для семафора.

Satan come and kill your family and you комментирует...

https://docs.python.org/2/library/fcntl.html#fcntl.lockf
Такой вариант вас тоже не устроит?

Ivan Markeev комментирует...

Не знаю, может сделаете тест сравнение?