Множества: уникальность и пересечения
Множество хранит уникальные значения без порядка. Идеально для тегов, уникальных посетителей и операций «общие друзья».
Если вам важно «есть ли элемент в наборе» и «нет дубликатов» — множество отвечает на оба вопроса за 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) превращается в дешёвое чтение готового множества.