Реализовать distributed lock для обновления памяти с использованием Redis Redlock
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать distributed lock для обновления памяти с использованием Redis Redlock
1. Цель задачи
Научиться проектировать и реализовывать распределённую блокировку (distributed lock) с помощью алгоритма Redlock на базе Redis для предотвращения race-conditions при параллельных обновлениях общей памяти мультиагентной системы. В результате должен получиться модуль, который гарантирует, что только один агент в каждый момент времени может модифицировать критическую секцию памяти, и исключает состояния гонки.
Ключевой результат Рабочая реализация distributed lock на Python с использованием Redis Redlock, интегрированная в симуляцию обновления памяти агентами, и набор тестов, подтверждающих отсутствие конфликтов.
2. Исходные данные
Перед началом необходимо иметь:
| Что нужно | Откуда взять |
|---|---|
| Запущенный экземпляр Redis (локально или в Docker) | Docker образ redis:7-alpine или установленный Redis 6+ |
| Python 3.10+ с возможностью установки пакетов | Среда разработки |
| Базовое понимание алгоритма Redlock | Документация Redis, статья «Distributed locks with Redis» |
| Шаблон мультиагентного взаимодействия (опционально) | Может быть взят из предыдущих задач (например, протокол распространения сообщений) |
Если нет реального инструмента — симулируем:
- Redis – запустить в Docker:
docker run -d --name redis-lock -p 6379:6379 redis:7-alpine - Мультиагентная среда – написать простую симуляцию: три воркера (threading или asyncio), которые периодически обновляют общий словарь (память). Для каждого обновления – захват блокировки.
- Контроль race-condition – без блокировки запустить симуляцию и убедиться, что происходит потеря данных (инкремент счётчика неверен). Это будет baseline.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.10+ | Основная реализация |
| Redis клиент | redis-py (>=5.0) | Взаимодействие с Redis |
| Redlock реализация | redlock-py (библиотека) или собственная реализация | Алгоритм распределённой блокировки |
| Тестирование | pytest, pytest-asyncio (если async) | Модульные и интеграционные тесты |
| Контейнеризация (опционально) | Docker Compose | Для нескольких Redis инстансов (если требуется истинный Redlock) |
| Логирование | structlog или logging | Отслеживание захвата/освобождения блокировок |
4. Этапы выполнения
Этап 1: Подготовка окружения и baseline (40 минут)
Действия
- Запустите Redis локально (Docker или native). Проверьте доступность:
redis-cli ping→PONG. - Создайте виртуальное окружение Python и установите зависимости:
python -m venv venv source venv/bin/activate pip install redis redlock-py pytest structlog - Напишите простую симуляцию без блокировки (файл
simulation_no_lock.py):- Общий словарь
shared_memory = {"counter": 0}. - Три потока (или asyncio задачи), каждый выполняет 1000 раз:
- Читает
counterиз словаря. - Увеличивает на 1.
- Записывает обратно.
- Читает
- После завершения всех потоков ожидаемое значение
counter= 3000.
- Общий словарь
- Запустите симуляцию несколько раз – убедитесь, что результат почти всегда меньше 3000 (race condition).
- Зафиксируйте в логах среднее отклонение.
Ожидаемый результат этапа Рабочий скрипт, демонстрирующий race condition; понимание проблемы.
Этап 2: Реализация Redlock (1 час)
Действия
- Создайте модуль
distributed_lock.py. - Используйте библиотеку
redlock-py(или реализуйте алгоритм вручную согласно спецификации):Redlockтребует список Redis-подключений по одному для каждого инстанса.- Для простоты используйте один Redis-инстанс (это компромисс – в реальном Redlock нужно >=3, но для учебной задачи допустимо).
- Класс
DistributedLockс методами:acquire(resource_identifier, ttl=10000)– пытается захватить блокировку на ресурс (например,"memory:update") с TTL 10 секунд. ВозвращаетTrue/False.release(lock)– освобождает блокировку, если она принадлежит нам.
- Используйте
uuidдля уникального идентификатора блокировки (lock token).
- Реализуйте правильное время удержания: если операция обновления заняла больше TTL, блокировка истекает автоматически (следует обрабатывать и перезахватывать).
- Добавьте логирование каждого успешного/неуспешного захвата и освобождения.
Ожидаемый результат этапа Класс DistributedLock, готовый к использованию.
Этап 3: Интеграция с обновлением памяти (1 час)
Действия
- Создайте новый скрипт
simulation_with_lock.py. - В каждом воркере перед чтением/записью
counter:- Вызвать
lock.acquire("memory:update"). - Если получен lock – выполнить операцию и сразу release.
- Если не получен – повторить попытку через случайную задержку (backoff).
- Вызвать
- Убедитесь, что воркеры используют единый экземпляр
DistributedLock(или создают свой, но с одинаковой конфигурацией Redis). - Параметры:
- Воркеры запускаются параллельно через
threading.Threadилиconcurrent.futures.ThreadPoolExecutor. - Каждый воркер делает 1000 операций.
- Воркеры запускаются параллельно через
- Запустите и проверьте, что итоговый counter == 3000 (или близко, если учесть отбрасывание операций из-за недоступности lock – нужно решить: либо ждать, либо отбрасывать; в учебной цели – ожидать).
Ожидаемый результат этапа Симуляция с блокировкой, которая даёт корректное значение счётчика (с незначительными потерями из-за ожидания блокировки, но без race).
Этап 4: Тестирование (45 минут)
Действия
- Напишите тесты в
test_distributed_lock.pyс использованиемpytest:- test_basic_acquire_release: захват и освобождение блокировки (один клиент).
- test_concurrent_contention: запуск 5 параллельных корутин/потоков, каждая пытается захватить блокировку и удержать её 0.1 сек. Проверьте, что нет одновременного удержания.
- test_lock_expiry: подтвердите, что блокировка автоматически истекает после TTL; другой клиент может её захватить.
- test_fencing_token (опционально): проверьте, что lock не может быть освобождён другим клиентом (безопасность).
- Запустите тесты:
pytest -v test_distributed_lock.py. - Исправьте все найденные ошибки.
Ожидаемый результат этапа Все тесты проходят; код покрыт на 80%+ (по зачётным сценариям).
Этап 5: Документация и анализ (30 минут)
Действия
- Напишите
README.md, в котором опишите: - Добавьте docstring в класс
DistributedLockи публичные методы. - Создайте файл
docker-compose.yml(опционально) для поднятия 3 Redis инстансов и запуска приложения.
Ожидаемый результат этапа Пакет с кодом, тестами и документацией.
5. Критерии приемки (Definition of Done)
- Реализован класс
DistributedLock, корректно реализующий протокол Redlock (захват, освобождение, timeout, расширение). - Написана симуляция обновления памяти тремя воркерами с использованием блокировки.
- Итоговое значение общего счётчика в симуляции с блокировкой стабильно равно ожидаемому (3000) с учётом допустимого отклонения <0.1% из-за отбрасывания операций.
- Все unit-тесты проходят (минимум 4 теста: захват/освобождение, конкурентность, истечение, безопасность).
- Код содержит логирование ключевых событий (захват, освобождение, ошибки).
- Прилагается README с описанием архитектуры, инструкцией по запуску и результатами сравнения.
- Код соответствует PEP8 (проверено
flake8илиblack).
6. Ожидаемый результат
- Основной артефакт: директория с проектом
distributed-lock-memory/, содержащая:distributed_lock.py– реализация распределённой блокировки.simulation_no_lock.py– демонстрация race condition.simulation_with_lock.py– демонстрация корректного обновления.test_distributed_lock.py– тесты.README.md– документация.requirements.txt– список зависимостей.- (Опционально)
docker-compose.ymlдля многократного Redis.
- Критерий успеха: при одновременном запуске трёх воркеров на 1000 операций каждый итоговый счётчик в памяти равен 3000 (с точностью до 0.1% потерь из-за конкурентных отказов захвата).
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Нестабильное поведение при TTL блокировки – операция обновления может длиться дольше TTL, и блокировка истекает, позволяя другому агенту войти | Использовать lock extension (периодически продлевать блокировку, пока идёт операция). Реализовать watchdog в отдельном потоке. |
| Один Redis-инстанс не даёт полной отказоустойчивости Redlock (по статье Мартина Клеппмана) | Для учебной задачи достаточно одного. В комментариях указать, что в production нужно ≥3 независимых Redis. |
| Гонка при освобождении блокировки – если клиент освобождает блокировку после того, как она уже была захвачена другим | Всегда проверять, что блокировка всё ещё принадлежит нам (сравнение lock token). |
| Проблемы с синхронизацией времени – Redlock использует таймстемпы на клиенте | Убедиться, что время на всех клиентах синхронизировано (например, через NTP). В локальной среде обычно не проблема. |
| Ошибки connection к Redis – клиент не может подключиться | Добавить retry-логику с exponential backoff; ловить исключения redis.ConnectionError. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Подготовка окружения и baseline | 40 минут |
| Этап 2: Реализация Redlock | 1 час |
| Этап 3: Интеграция с обновлением памяти | 1 час |
| Этап 4: Тестирование | 45 минут |
| Этап 5: Документация и анализ | 30 минут |
| Итого (чистое время) | ~4 часа |
| Примечание для первого раза | +1 час на изучение Redlock и решение неожиданных проблем |
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 42 | «Что такое distributed lock и зачем он нужен?» |
| 43 | «Алгоритм Redlock: плюсы и минусы» |
| 44 | «Race condition в параллельных системах» |
| 101 | «Использование Redis как брокера сообщений» |
| 199 | (текущая задача) |
| 233 | «Проблема split-brain в распределённых системах» |
| 278 | «CAP-теорема и её влияние на блокировки» |
| 301 | «Idempotent operations и fencing tokens» |
| 402 | «Тайм-ауты и retry-логика в распределённых системах» |
| 500 | «Docker Compose для инфраструктуры Redis» |
10. Чек-лист самопроверки
- Я понимаю, что такое distributed lock и почему он необходим для обновления общей памяти агентами.
- Я написал baseline без блокировки и убедился, что race condition существует.
- Я реализовал класс
DistributedLockна основе Redlock (любая реализация: библиотечная или собственная). - Я интегрировал блокировку в симуляцию и добился корректного итогового счётчика.
- Я написал тесты, которые проверяют корректность блокировки (конкурентность, истечение, безопасность) и они проходят.
- Я задокументировал проект (README, docstring) и указал ограничения.
- Я убедился, что код соответствует PEP8 и не содержит явных багов.