Списки: очереди и стеки

Список Redis — это двусторонняя очередь. Из неё легко сделать очередь задач, стек или ленту последних событий.

Список Redis устроен как дек: добавлять и забирать элементы с обоих концов за O(1). Этого достаточно, чтобы построить очередь задач промышленного уровня.

Список (List) хранит упорядоченную последовательность строк. Главная особенность — быстрые операции с обоих концов. Это делает список идеальным для очередей, стеков и лент новостей.

Основные команды

127.0.0.1:6379> RPUSH tasks "task1"
(integer) 1
127.0.0.1:6379> RPUSH tasks "task2" "task3"
(integer) 3
127.0.0.1:6379> LRANGE tasks 0 -1
1) "task1"
2) "task2"
3) "task3"
127.0.0.1:6379> LPOP tasks
"task1"
127.0.0.1:6379> LLEN tasks
(integer) 2

RPUSH — добавить справа (в хвост), LPUSH — слева (в голову). LPOP/RPOP — забрать с соответствующего конца. LRANGE 0 -1 — показать весь список. LLEN — длина.

Очередь vs стек

   Очередь (FIFO):  RPUSH добавляет, LPOP забирает
   вход -> [a][b][c] -> выход
   RPUSH d:  [a][b][c][d]
   LPOP:     забрали a   (первый вошёл - первый вышел)

   Стек (LIFO):  RPUSH добавляет, RPOP забирает
   RPUSH d:  [a][b][c][d]
   RPOP:     забрали d   (последний вошёл - первый вышел)

Блокирующие операции

BLPOP/BRPOP — блокирующие версии. Если список пуст, клиент ждёт, пока кто-то не добавит элемент (или не истечёт таймаут). Это превращает список в простую очередь задач: воркеры висят на BLPOP и просыпаются, когда появляется работа.

BRPOP tasks 5     # ждать до 5 секунд появления элемента

Демонстрация: очередь задач на Python deque

Список Redis по поведению — это collections.deque. Смоделируем очередь задач с воркером:

from collections import deque

# Очередь задач — аналог списка Redis
queue = deque()

# Продюсеры добавляют задачи (как RPUSH)
for i in range(1, 6):
    queue.append(f"job-{i}")
    print(f"RPUSH job-{i}  ->  длина очереди: {len(queue)}")

print("\n--- Воркер разбирает очередь (как LPOP) ---")
# Воркер забирает задачи слева в порядке FIFO
while queue:
    task = queue.popleft()   # аналог LPOP
    print(f"LPOP -> обрабатываю {task}, осталось: {len(queue)}")

print("\nОчередь пуста. В Redis воркер ждал бы на BLPOP.")
print("FIFO: задачи обработаны в том же порядке, что добавлены.")

Тот же принцип FIFO лежит в основе очередей задач на Redis: один процесс кладёт работу через RPUSH, другой забирает через LPOP или BLPOP.

Как работает под капотом

Современный Redis хранит списки как quicklist — двусвязный список, где каждый узел представляет собой компактный listpack (раньше — ziplist). Маленькие списки целиком умещаются в один listpack, экономя память. По мере роста добавляются узлы. Благодаря двусвязной структуре операции с обоих концов — O(1), а доступ к середине через LINDEX — O(n).

Частые ошибки

  • Использовать список как массив с произвольным доступом. LINDEX по середине — O(n), это медленно для длинных списков.
  • Не ограничивать рост списка. Лента «последних событий» без LTRIM разрастётся бесконечно.
  • Поллинг вместо BLPOP. Крутить цикл с проверкой LLEN — расточительно; блокирующие операции эффективнее.

Best practices

  • Для очередей задач используйте RPUSH + BLPOP.
  • Ограничивайте длину лент через LTRIM list 0 99 (оставить последние 100).
  • Для надёжных очередей с подтверждениями рассмотрите Redis Streams (раздел про очереди).

Итог: Список — двусторонняя очередь с O(1) на концах. RPUSH/LPOP дают FIFO-очередь, RPUSH/RPOP — стек. BLPOP делает из списка очередь задач. Внутри — эффективный quicklist из listpack-узлов.

Управление длиной и доступ к элементам

На практике списки почти всегда нужно ограничивать по длине, иначе лента событий или лог разрастаются бесконечно. Для этого есть LTRIM, обрезающий список до диапазона:

LPUSH feed:user42 "новое событие"
LTRIM feed:user42 0 99      # оставить только 100 последних
LRANGE feed:user42 0 9      # показать 10 самых свежих
LINDEX feed:user42 0        # элемент по индексу (O(n) для середины!)
LSET feed:user42 0 "правка" # заменить элемент по индексу

Связка LPUSH + LTRIM — это классический рецепт «ленты последних N событий»: добавляем в голову и тут же отсекаем хвост. Память под контролем, а свежие элементы всегда сверху.

Важно держать в голове сложность операций. Добавление и извлечение с концов — O(1). А вот LINDEX по середине, LINSERT и LREM — O(n), потому что списку приходится идти по цепочке узлов. Если вам постоянно нужен произвольный доступ по индексу — список не та структура; присмотритесь к sorted set или хешу. Список силён именно как очередь и стек, где работа идёт с концами.

Проверьте себя
1. Как из списка Redis сделать FIFO-очередь (первый вошёл — первый вышел)?
ARPUSH для добавления и RPOP для извлечения
BRPUSH для добавления и LPOP для извлечения
CLPUSH для добавления и LPOP для извлечения
DСписки не подходят для очередей
2. Чем BLPOP отличается от LPOP?
ABLPOP работает только с числами
BBLPOP блокирует клиента и ждёт появления элемента, если список пуст
CBLPOP забирает элемент с правого конца
DBLPOP удаляет весь список целиком