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 на дневных ключах и помните, что большое смещение сразу аллоцирует всю строку.
Проверьте себя
1. Что на самом деле представляет собой bitmap в Redis?
AОтдельный тип данных со своей структурой в памяти
BНабор битовых команд поверх обычного строкового значения
CРазновидность sorted set, где score — это бит
DСпециальный индекс на множестве (set)
2. Пользователю с id 11 дважды сделали SETBIT online 11 1. Что покажет BITCOUNT online?
AУчтёт оба раза, прибавив 2
BУчтёт его один раз — повторная установка бита ничего не меняет
CВернёт ошибку из-за дублирования
DСбросит бит обратно в 0
3. В каком случае bitmap — плохой выбор для подсчёта уникальных?
AКогда id — плотные целые числа от нуля
BКогда нужно объединить несколько дней через BITOP
CКогда идентификаторы разрежены (например, UUID или id до миллиарда при тысяче пользователей)
DКогда требуется узнать общее число единиц