Bitmaps: биты как данные
Bitmap в Redis — это обычная строка, к которой обращаются по номеру отдельного бита; так миллионы флагов «да/нет» умещаются в килобайты.
Bitmap — не отдельный тип данных, а набор битовых команд (
SETBIT,GETBIT,BITCOUNT,BITOP) поверх строкового значения. Каждый бит по своему смещению хранит ровно один признак: 1 — «было», 0 — «не было».
Базовые структуры вы уже знаете: строки, списки, хэши, множества, sorted set. Bitmaps — это первый из «продвинутых» инструментов, и он решает узкую, но очень частую задачу: компактно отвечать на вопрос «случилось ли событие X для объекта с числовым id». Был ли пользователь онлайн сегодня? Прочитал ли он статью? Прошёл ли шаг онбординга? Когда таких объектов миллионы, хранить по флагу на каждого в обычных ключах разорительно — а один bitmap уложит миллион признаков примерно в 122 КБ.
Зачем это на практике
Классический сценарий — аналитика присутствия (presence analytics). Заведите по одному ключу на каждый день: online:2026-06-27. Когда пользователь с id 4242 заходит, вы ставите бит под смещением 4242 в единицу. В конце дня BITCOUNT мгновенно скажет, сколько уникальных пользователей было активно, а BITOP AND по семи дневным ключам — сколько заходили каждый день недели (ядро лояльной аудитории). Никаких GROUP BY по таблице событий на миллиарды строк.
SETBIT и GETBIT: один бит за раз
Команда SETBIT key offset value ставит бит по смещению (value — только 0 или 1) и возвращает прежнее значение бита. Если строка короче, Redis сам дополнит её нулями до нужной длины. GETBIT key offset читает бит; для смещения за пределами строки вернёт 0.
# отметили активность пользователей 0, 5 и 11 за сегодня
SETBIT online:2026-06-27 0 1 # => 0 (прежнее значение бита)
SETBIT online:2026-06-27 5 1 # => 0
SETBIT online:2026-06-27 11 1 # => 0
GETBIT online:2026-06-27 5 # => 1 заходил
GETBIT online:2026-06-27 7 # => 0 не заходил
Смещение задаёт сам объект: бит номер N отвечает за пользователя с id N. Поэтому удобнее всего, когда id — плотные целые числа от нуля. Если id разрежены (например, UUID), bitmap не подойдёт — там лучше HyperLogLog из следующего урока.
BITCOUNT: сколько единиц
BITCOUNT key считает количество выставленных битов — то есть число уникальных объектов с признаком. Можно ограничить диапазоном байтов, а с модификатором BIT — диапазоном битов.
BITCOUNT online:2026-06-27 # сколько уникальных за день
BITCOUNT online:2026-06-27 0 0 # только в первом байте (id 0..7)
BITCOUNT online:2026-06-27 0 100 BIT # в первых 101 битах (id 0..100)
BITOP: пересечения и объединения дней
BITOP делает побитовые операции AND, OR, XOR, NOT над несколькими bitmap и кладёт результат в новый ключ. Это и есть та самая «аналитика когорт» без базы данных.
# кто заходил И в понедельник, И во вторник
BITOP AND active:both online:2026-06-22 online:2026-06-23
BITCOUNT active:both # ядро ежедневной аудитории
# кто заходил хотя бы в один из двух дней
BITOP OR active:any online:2026-06-22 online:2026-06-23
BITCOUNT active:any
Как это работает под капотом
Bitmap — это байтовый буфер (та же строка SDS, что и у обычных строковых значений). Бит под смещением offset лежит в байте offset / 8, а внутри байта Redis нумерует биты от старшего к младшему: позиция 7 - (offset % 8). SETBIT — это маска OR либо AND NOT по одному байту, BITCOUNT — суммирование popcount по всем байтам (на процессоре это инструкция POPCNT, очень быстро). Чтобы прочувствовать механику, соберём мини-bitmap на чистом Python — без Redis, на bytearray:
class BitArray:
def __init__(self):
self.data = bytearray()
def setbit(self, offset, value):
byte_index = offset // 8
bit_index = 7 - (offset % 8) # старший бит слева, как в Redis
while len(self.data) <= byte_index:
self.data.append(0)
if value:
self.data[byte_index] |= (1 << bit_index)
else:
self.data[byte_index] &= ~(1 << bit_index)
def getbit(self, offset):
byte_index = offset // 8
bit_index = 7 - (offset % 8)
if byte_index >= len(self.data):
return 0
return (self.data[byte_index] >> bit_index) & 1
def bitcount(self):
return sum(bin(b).count("1") for b in self.data)
users = BitArray()
for uid in (0, 5, 11, 11): # user 11 отметили дважды
users.setbit(uid, 1)
print("Заходил ли user 5? ", users.getbit(5))
print("Заходил ли user 7? ", users.getbit(7))
print("Уникальных за день: ", users.bitcount())
print("Байт под 12 id: ", len(users.data))
Вывод:
Заходил ли user 5? 1 Заходил ли user 7? 0 Уникальных за день: 3 Байт под 12 id: 2
Обратите внимание: пользователя 11 мы пометили дважды, а bitcount вернул 3 — повторная установка того же бита ничего не меняет. Именно это даёт «бесплатную» дедупликацию: bitmap считает уникальных, а не события. И двенадцать признаков уместились всего в 2 байта.
Экономия памяти
Сравните прямой подход «ключ на пользователя» и bitmap для миллиона id за один день.
| Подход | Память на 1 млн признаков |
Отдельный ключ online:<id> = 1 | десятки МБ (накладные расходы на каждый ключ) |
Множество SADD online <id> | несколько МБ (хранит сами id) |
Bitmap SETBIT online <id> 1 | ≈ 122 КБ (1 млн бит / 8) |
Частые ошибки
- Разрежённые id. Если максимальный id — миллиард, bitmap раздуется до ~119 МБ, даже если реальных пользователей тысяча. Bitmaps хороши только для плотных смещений; для разрежённых считайте уникальных через HyperLogLog.
- Строковые ключи как смещение.
SETBITпринимает только числовое смещение. Если идентификатор — email или UUID, его сначала надо отобразить в целое (отдельной нумерацией), иначе bitmap не применить. - Путаница value и offset. В
SETBIT key offset valueтретий аргумент — это бит (0/1), а не «во сколько раз». Передать туда что-то кроме 0 или 1 — ошибка. - Забыть про TTL. Дневные ключи присутствия копятся вечно. Ставьте
EXPIREна разумный срок (например, 90 дней), иначе память утечёт на истории. - Большой одиночный SETBIT. Установка бита по огромному смещению мгновенно аллоцирует строку до этого размера — один
SETBIT k 4000000000 1попросит у Redis полгигабайта.
Итоги
- Bitmap — это битовые команды поверх обычной строки; бит N хранит признак для объекта с id N.
SETBIT/GETBITработают с одним битом,BITCOUNTсчитает единицы,BITOPделает AND/OR/XOR между bitmap.- Идеальный сценарий — аналитика присутствия и когорт по плотным числовым id: миллион флагов в ~122 КБ.
- Bitmap считает уникальные объекты (повторная установка бита бесплатна), но не годится для разрежённых id и нечисловых ключей.
- Не забывайте TTL на дневных ключах и помните, что большое смещение сразу аллоцирует всю строку.