Множества: уникальность и пересечения

Множество хранит уникальные значения без порядка. Идеально для тегов, уникальных посетителей и операций «общие друзья».

Если вам важно «есть ли элемент в наборе» и «нет дубликатов» — множество отвечает на оба вопроса за O(1).

Множество (Set) — это коллекция уникальных строк без определённого порядка. Добавить дубликат нельзя — он просто проигнорируется. Проверка принадлежности молниеносна. А ещё над множествами можно делать операции из теории множеств: пересечение, объединение, разность.

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

127.0.0.1:6379> SADD tags "redis" "cache" "nosql"
(integer) 3
127.0.0.1:6379> SADD tags "redis"
(integer) 0
127.0.0.1:6379> SISMEMBER tags "cache"
(integer) 1
127.0.0.1:6379> SMEMBERS tags
1) "redis"
2) "cache"
3) "nosql"
127.0.0.1:6379> SCARD tags
(integer) 3
127.0.0.1:6379> SREM tags "nosql"
(integer) 1

SADD добавляет (возвращает число реально добавленных), SISMEMBER проверяет принадлежность за O(1), SMEMBERS показывает всё, SCARD — размер, SREM удаляет.

Операции над множествами

SADD user:1:friends "alice" "bob" "carol"
SADD user:2:friends "bob" "carol" "dave"

SINTER user:1:friends user:2:friends   # общие: bob, carol
SUNION user:1:friends user:2:friends   # все: alice..dave
SDIFF  user:1:friends user:2:friends   # только у 1: alice

Это превращает Redis в инструмент для соцграфов: «общие друзья» — это SINTER, «друзья друзей» — комбинация операций.

   SINTER (пересечение):    SUNION (объединение):
   A = {alice, bob, carol}  A | B = все уникальные
   B = {bob, carol, dave}   = {alice,bob,carol,dave}

      A         B
   [alice]  [bob ]  [dave]
           [carol]
   общее: bob, carol

Демонстрация: множества и их операции на Python set

Множество Redis — это прямой аналог встроенного set в Python. Логика операций идентична:

# Друзья двух пользователей — как множества Redis
friends_1 = set()
for f in ["alice", "bob", "carol", "bob"]:  # bob дублируется
    added = f not in friends_1
    friends_1.add(f)
    print(f"SADD user:1 {f}  ->  добавлен: {int(added)}")

friends_2 = {"bob", "carol", "dave"}

print("\n--- Операции над множествами ---")
print("Общие друзья (SINTER):  ", sorted(friends_1 & friends_2))
print("Все друзья   (SUNION):  ", sorted(friends_1 | friends_2))
print("Только у 1   (SDIFF):   ", sorted(friends_1 - friends_2))
print("Размер user:1 (SCARD):  ", len(friends_1))
print("\nДубликат bob был проигнорирован — множество хранит уникальные.")

Обратите внимание: повторное добавление bob ничего не изменило. Так же ведёт себя SADD в Redis, возвращая 0 для уже существующих элементов.

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

Redis выбирает кодировку множества автоматически. Если все элементы — целые числа и их немного, используется intset — отсортированный массив целых, крайне компактный. Для маленьких множеств строк — listpack. Когда множество растёт или содержит длинные строки, Redis переключается на хеш-таблицу, где SISMEMBER работает за O(1). Переключение прозрачно для вас.

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

  • SMEMBERS на огромном множестве. Вернёт все элементы разом, блокируя сервер. Используйте SSCAN для обхода.
  • SINTER по большим множествам в горячем пути. Это O(n) и может быть тяжёлым; кэшируйте результат через SINTERSTORE.
  • Ожидать порядок. Множество неупорядочено; если нужен порядок — это sorted set.

Best practices

  • Используйте множества для уникальности: теги, уникальные посетители, набор прав.
  • Для подсчёта уникальных при огромных объёмах рассмотрите HyperLogLog (раздел о продакшене).
  • Тяжёлые пересечения сохраняйте через SINTERSTORE и переиспользуйте.

Итог: Множество — уникальные значения без порядка, с O(1) проверкой принадлежности и операциями пересечения/объединения/разности. Внутри — intset, listpack или хеш-таблица в зависимости от данных.

Случайные элементы и сохранение результатов

У множеств есть команды, которые делают их удобными не только для проверки уникальности. Например, можно достать случайный элемент — основа для «случайной рекомендации» или розыгрыша:

SRANDMEMBER tags 3      # 3 случайных элемента (без удаления)
SPOP tags 1             # достать и УДАЛИТЬ случайный элемент
SMOVE src dst "redis"   # атомарно перенести элемент между множествами

SPOP особенно полезен, когда множество выступает как «мешок задач»: воркеры по одному вытаскивают из него элементы, и каждый достаётся только одному. SRANDMEMBER же не удаляет — он для подглядывания.

Тяжёлые операции над множествами стоит материализовать. Команды SINTERSTORE, SUNIONSTORE и SDIFFSTORE не просто вычисляют результат, а сохраняют его в новый ключ. Это позволяет посчитать, скажем, пересечение больших множеств один раз, положить результат с коротким TTL и отдавать его множеству запросов без повторного вычисления. Так дорогая операция O(n) превращается в дешёвое чтение готового множества.

Проверьте себя
1. Что вернёт SADD, если добавляемый элемент уже есть в множестве?
AОшибку дубликата
B1, перезаписав элемент
C0, так как реально ничего не добавлено
Dnil
2. Какая команда найдёт общих друзей двух пользователей, хранящихся в множествах?
ASUNION
BSINTER
CSDIFF
DSMEMBERS