Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Базы данных · Уровень: Senior ## TL;DR - **NoSQL** — это не «отсутствие схемы», а класс хранилищ с разными моделями данных, оптимизированных под конкретные паттерны доступа: Key-Value (Redis, DynamoDB), Document (MongoDB), Wide-column (Cassandra, HBase), Graph (Neo4j). Выбор движется от паттернов запросов, а не от данных. - **SQL vs NoSQL** — не «или-или». SQL даёт строгую схему, JOIN-ы, ACID, нормализацию; NoSQL — горизонтальное масштабирование, гибкую схему, денормализацию под чтение. По **CAP** при сетевом разделении (P неизбежно) выбираешь между C и A; многие NoSQL — это **AP** с **BASE**-семантикой (eventual consistency), но это конфигурируемо (Cassandra tunable consistency, DynamoDB strongly consistent reads). - **Redis** — однопоточное (для исполнения команд) in-memory хранилище структур данных. Сила не в «кэше», а в богатых структурах: String, List, Hash, Set, ZSET, Bitmap, HyperLogLog, Stream, geo. Атомарность каждой команды — следствие single-threaded модели. - **Персистентность**: RDB (снапшоты, компактно, риск потери данных между снапшотами), AOF (журнал команд, durability до `appendfsync everysec`/`always`), гибрид (RDB-преамбула + AOF-хвост) — дефолт в Redis 7. - **Кэширование**: cache-aside (самый частый), write-through/write-back; проблемы — thundering herd / cache stampede (защита: lock на пересчёт, jitter в TTL, early recomputation), stale data / invalidation. - **Distributed lock**: `SET key val NX PX ttl` + unlock через Lua (проверка владельца). Redlock спорен (критика Martin Kleppmann — fencing token нужен всё равно); для строгих гарантий не полагайся только на TTL-lock. - **Rate limiting**: token bucket / sliding window log / sliding window counter на Redis + Lua для атомарности. - **Масштабирование**: репликация (async, replica может отставать), Redis Cluster (16384 хеш-слота, без multi-key операций между слотами без hash tags). Eviction: `allkeys-lru`, `allkeys-lfu`, `volatile-*`, `noeviction`. --- ## Теория ### Часть 1. NoSQL #### Что такое NoSQL и зачем он появился Реляционные СУБД оптимизированы под нормализованные данные, произвольные запросы (ad-hoc) и строгую консистентность на одном узле. Проблемы возникают при: - **горизонтальном масштабировании** — шардирование SQL с JOIN-ами и распределёнными транзакциями дорого; - **гибкой/эволюционирующей схеме** — миграции `ALTER TABLE` на больших таблицах болезненны; - **огромных объёмах записи** с предсказуемыми паттернами доступа. NoSQL — это семейство хранилищ, каждое из которых жертвует какой-то «универсальностью» SQL ради масштабирования и/или производительности на конкретном паттерне. Ключевая идея senior-уровня: **в NoSQL ты проектируешь данные под запросы (query-first / access-pattern-driven design), а не под сущности**. #### Типы NoSQL и их модели данных | Тип | Модель данных | Примеры | Сильная сторона | Кейсы | |---|---|---|---|---| | Key-Value | `key → opaque value` | Redis, DynamoDB, Riak | O(1) доступ по ключу, простое шардирование | кэш, сессии, профили, корзины, feature flags | | Document | `key → JSON/BSON документ` | MongoDB, Couchbase | вложенные структуры, частичные запросы по полям, индексы | каталоги, CMS, профили с переменной структурой | | Wide-column | `row key → column families (sparse, wide)` | Cassandra, HBase, ScyllaDB | огромная пропускная запись, линейное масштабирование, time-series | логи, метрики, IoT, event store, feed | | Graph | узлы + рёбра + свойства | Neo4j, JanusGraph | обход связей (traversal), запросы по отношениям | соцграфы, рекомендации, fraud detection, knowledge graph | **Key-Value (Redis/DynamoDB).** Простейшая модель: положил по ключу — достал по ключу. DynamoDB добавляет partition key + sort key (composite), GSI/LSI для альтернативных паттернов доступа. Главное ограничение — нет богатых запросов: всё проектируется вокруг ключей. В DynamoDB single-table design — норма. **Document (MongoDB).** Документ — самодостаточная единица (агрегат в терминах DDD). Сильно: `db.users.find({ "address.city": "Berlin" })`, вложенные массивы, частичные обновления. Подводный камень — соблазн денормализации без понимания границ агрегата приводит к большим документам и дублированию. У MongoDB есть multi-document транзакции (с 4.0), но их использование часто сигнал о неверной модели. **Wide-column (Cassandra/HBase).** Не «таблица с колонками» в SQL-смысле. Это map of maps: `partition key → (clustering columns → values)`. Запросы эффективны только по первичному ключу и в порядке clustering columns. Cassandra: masterless, tunable consistency (`ONE`, `QUORUM`, `ALL`), оптимизирована под write-heavy (LSM-tree). Моделирование — строго от запросов: одна таблица на паттерн доступа, денормализация ожидаема. **Graph (Neo4j).** Когда «связи — это данные первого класса». Запрос «друзья друзей, купившие X» в SQL — каскад JOIN-ов с экспоненциальной деградацией; в графовой БД — index-free adjacency, обход за O(степень узла). Cypher: `MATCH (a:User)-[:FRIEND*2]->(b)-[:BOUGHT]->(p:Product {id:$id}) RETURN b`. #### Когда NoSQL, а когда SQL | Критерий | SQL | NoSQL | |---|---|---| | Схема | строгая, нормализованная, на запись | гибкая/schema-on-read, денормализованная под чтение | | Запросы | произвольные ad-hoc, JOIN, агрегации | по заранее известным паттернам доступа | | Масштабирование | вертикальное; шардинг сложен | горизонтальное «из коробки» | | Транзакции | ACID, multi-row, multi-table | ограниченные (часто на уровне одного ключа/документа/partition) | | Консистентность | strong по умолчанию | часто eventual (настраиваемо) | | Целостность | FK, constraints на уровне БД | на уровне приложения | **Бери SQL по умолчанию**, если: нужны произвольные аналитические запросы, строгие транзакции/инварианты между сущностями, объёмы укладываются в один узел или разумный шардинг, схема стабильна. Postgres + JSONB сегодня закрывает значительную долю кейсов «нам нужен документный store». **Бери NoSQL**, когда есть конкретная причина: предсказуемый паттерн доступа + потребность в линейном масштабировании (Cassandra/DynamoDB), in-memory скорость и структуры данных (Redis), естественно графовая задача (Neo4j), variable schema документов (MongoDB). #### CAP, PACELC, BASE vs ACID - **CAP**: при сетевом разделении (**P**) система может гарантировать либо консистентность (**C**), либо доступность (**A**), но не обе. P в распределённой системе неизбежно — значит реальный выбор C vs A *во время partition*. Важно: CAP про поведение **при разделении**, а не «вообще». «CA-система» — это, по сути, одноузловая. - **PACELC** (расширение Abadi): if **P** then C/A, **E**lse (в нормальном режиме) — latency (**L**) vs consistency (**C**). Например DynamoDB/Cassandra по умолчанию PA/EL (доступность и низкая задержка), Spanner — PC/EC (консистентность, ценой latency). - **ACID** (Atomicity, Consistency, Isolation, Durability) — гарантии классических СУБД. - **BASE** (Basically Available, Soft state, Eventual consistency) — философия многих NoSQL: система всегда отвечает, состояние может быть временно несогласованным, со временем сходится. Важный нюанс senior-уровня: «NoSQL = eventual consistency» — упрощение. Cassandra даёт tunable consistency (`R + W > N` → strong), DynamoDB — strongly consistent reads опцией, MongoDB — read/write concern (`majority`). Консистентность — это спектр и часто per-operation настройка, а не свойство БД целиком. #### Типичные ошибки выбора NoSQL - **«Schemaless = без проектирования»** — на деле схема перемещается в код и становится неявной; разные версии документов сосуществуют и порождают баги. - **Выбор под хайп, а не под паттерн доступа** — MongoDB «потому что JSON», хотя нужны транзакции и JOIN. - **Игнор паттернов запросов в Cassandra/DynamoDB** — спроектировали по сущностям, потом нужен запрос «не по ключу» → full scan / дорогие GSI. - **Ожидание strong consistency от AP-системы по умолчанию** — read-after-write баги. - **Графовая задача в реляционке** (рекурсивные CTE на глубоких обходах деградируют) или наоборот, таблично-аналитическая задача в графе. - **Redis как primary database без понимания durability** — потеря данных при сбое между fsync. --- ### Часть 2. Redis #### Модель исполнения: single-threaded Redis исполняет команды в **одном потоке** (event loop на epoll/kqueue). Следствия: - **каждая команда атомарна** — нет гонок между командами, не нужны блокировки для одиночной операции; - **нет накладных расходов на синхронизацию** между потоками; узкое место — CPU одного ядра и сеть; - **долгие команды блокируют всё** — `KEYS *`, `SMEMBERS` на огромном сете, неоптимальный Lua-скрипт «вешают» весь инстанс. Используй `SCAN`/`HSCAN`/`SSCAN` (курсорные, неблокирующие). Современный Redis (6+) многопоточен для **I/O** (чтение/парсинг/запись в сокеты) и для фоновых задач (удаление больших ключей `UNLINK`, сохранение RDB через `fork`), но логика команд по-прежнему сериализуется. Это и обеспечивает простую модель консистентности. #### Структуры данных, команды и кейсы | Структура | Ключевые команды | Сложность | Типичные кейсы | |---|---|---|---| | String | `SET/GET`, `INCR`, `SETEX`, `SET NX PX`, `GETSET` | O(1) | кэш, счётчики, флаги, distributed lock | | List | `LPUSH/RPUSH`, `LPOP/RPOP`, `BLPOP`, `LRANGE` | O(1) края | очереди, стек, recent items | | Hash | `HSET/HGET`, `HGETALL`, `HINCRBY` | O(1) поле | объекты/записи (профиль = hash) | | Set | `SADD`, `SISMEMBER`, `SINTER`, `SUNION` | O(1)/O(N) | теги, уникальные посетители, отношения | | Sorted Set (ZSET) | `ZADD`, `ZRANGE`, `ZRANGEBYSCORE`, `ZRANK` | O(log N) | leaderboard, приоритетные очереди, sliding window | | Bitmap | `SETBIT`, `GETBIT`, `BITCOUNT`, `BITOP` | O(1)/O(N) | daily active users, флаги по id | | HyperLogLog | `PFADD`, `PFCOUNT`, `PFMERGE` | O(1), ~12KB | приблизительный count-distinct (unique visitors) | | Stream | `XADD`, `XREAD`, `XREADGROUP`, `XACK` | O(1) добавление | event log, очереди с consumer groups | | Geospatial | `GEOADD`, `GEOSEARCH`, `GEODIST` | O(log N) | «рядом со мной», доставка | **String.** Не только текст — это байты до 512 МБ. `INCR`/`INCRBY` атомарны → счётчики просмотров, генерация ID. `SET key val EX 60 NX` — атомарная установка с TTL только если ключа нет (основа lock). **List.** Двусвязный список (quicklist). `LPUSH` + `BRPOP` → надёжная-ish очередь (producer/consumer). `BLPOP` блокирует клиента до появления элемента — без busy-polling. **Hash.** Хранение объекта одним ключом без сериализации всего JSON: `HSET user:42 name Ann age 30`, обновление одного поля `HINCRBY user:42 age 1`. Память эффективнее, чем String-на-поле, при малых hash (ziplist/listpack-кодировка). **Set / Sorted Set.** Set — членство и операции множеств (общие друзья = `SINTER`). ZSET — каждый элемент со score, отсортирован. Leaderboard: `ZADD lb 1500 player1`, топ-10: `ZREVRANGE lb 0 9 WITHSCORES`, ранг: `ZREVRANK lb player1`. ZSET — основа sliding-window rate limit и приоритетных очередей. **Bitmap.** Один бит на сущность. DAU: `SETBIT dau:2026-06-14 1`, число активных — `BITCOUNT`. Для 1М пользователей — 125 КБ. Retention-анализ через `BITOP AND`. **HyperLogLog.** Вероятностная структура для cardinality с ~0.81% ошибкой и фиксированными ~12 КБ независимо от объёма. `PFADD visitors:2026-06-14 user1 user2` / `PFCOUNT`. Когда точность не критична, а Set был бы гигантским. **Stream.** Append-only лог с ID `-`, consumer groups (как Kafka-lite): несколько consumer'ов делят нагрузку, `XACK` подтверждает обработку, `XPENDING`/`XCLAIM` — переобработка «зависших». Замена List-очередям, где нужны acknowledgement, replay и несколько групп потребителей. **Geospatial.** Поверх ZSET (geohash как score). `GEOADD`, поиск в радиусе `GEOSEARCH ... BYRADIUS 5 km`. ```go // go-redis (github.com/redis/go-redis/v9): базовые операции import ( "context" "time" "github.com/redis/go-redis/v9" ) rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) ctx := context.Background() // String + TTL rdb.Set(ctx, "user:42:token", "abc", 30*time.Minute) // Атомарный счётчик views, _ := rdb.Incr(ctx, "post:1:views").Result() // Hash как объект rdb.HSet(ctx, "user:42", "name", "Ann", "age", 30) // Sorted set — leaderboard rdb.ZAdd(ctx, "lb", redis.Z{Score: 1500, Member: "player1"}) top, _ := rdb.ZRevRangeWithScores(ctx, "lb", 0, 9).Result() _ = views _ = top ``` #### Персистентность | Механизм | Что делает | Плюсы | Минусы / durability | |---|---|---|---| | RDB | бинарный снапшот всего датасета по расписанию (`save 900 1`) через `fork()` | компактен, быстрый рестарт, мало влияет на runtime | теряются данные между снапшотами (минуты); `fork` дорог при больших датасетах (copy-on-write память) | | AOF | журнал всех модифицирующих команд | минимальная потеря (`everysec` ≤1с, `always` — почти 0) | файл больше, рестарт медленнее (реплей лога) | | Гибрид (Redis 7) | AOF с RDB-преамбулой: компактный снапшот + журнал хвоста | быстрый рестарт + хорошая durability | компромисс по умолчанию | `appendfsync`: - `always` — fsync на каждую запись: максимальная durability, минимальный throughput. - `everysec` (дефолт) — fsync раз в секунду: потеря ≤1 секунды при сбое, хороший баланс. - `no` — fsync отдаёт ОС: быстро, но окно потери — неопределённое. **Главный тезис для senior:** даже с AOF `always` Redis не даёт гарантий уровня традиционной СУБД при репликации, потому что **репликация асинхронна** — подтверждённая клиенту запись может потеряться при failover, если не реплицировалась (см. `WAIT`, но это не панацея). Поэтому Redis как single source of truth для критичных данных — осознанный риск. #### Pipelining Несколько команд отправляются одним батчем без ожидания ответа на каждую → экономия RTT. Это **не** транзакция (нет атомарности/изоляции), просто батчинг сети. ```go pipe := rdb.Pipeline() incr := pipe.Incr(ctx, "counter") pipe.Expire(ctx, "counter", time.Hour) _, _ = pipe.Exec(ctx) // один round-trip _ = incr.Val() ``` #### Transactions: MULTI / EXEC / WATCH `MULTI` начинает транзакцию, команды ставятся в очередь, `EXEC` исполняет их **атомарно и последовательно** (никто не вклинится — single-threaded). НО: это **не** ACID-транзакция SQL — нет rollback при логической ошибке (если команда выполнилась с ошибкой типа, остальные всё равно выполняются). `WATCH` даёт **оптимистичную блокировку**: если watched-ключ изменился между `WATCH` и `EXEC`, транзакция отменяется (`EXEC` возвращает nil) — это CAS-паттерн. ```go // Оптимистичный декремент остатка через WATCH (CAS) key := "stock:item1" err := rdb.Watch(ctx, func(tx *redis.Tx) error { n, err := tx.Get(ctx, key).Int() if err != nil { return err } if n <= 0 { return errors.New("out of stock") } _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { p.Decr(ctx, key) return nil }) return err // redis.TxFailedErr → ретрай при конфликте }, key) _ = err ``` #### Lua-скрипты Скрипт исполняется **атомарно** (как единая команда, блокирует loop на время выполнения) и серверно — идеален для read-modify-write без race и без сетевых RTT. `EVAL`/`EVALSHA` (по SHA1 закэшированного скрипта). Используется для надёжного unlock, rate limiter, conditional updates. ```go // Атомарный unlock: удаляем ключ только если значение наше (наш токен) var unlock = redis.NewScript(` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end`) res, _ := unlock.Run(ctx, rdb, []string{"lock:order:1"}, myToken).Int() _ = res ``` #### Кэширование **Cache-aside (lazy loading)** — самый частый. Приложение само управляет кэшем: ```go func GetUser(ctx context.Context, rdb *redis.Client, db DB, id string) (User, error) { key := "user:" + id if s, err := rdb.Get(ctx, key).Result(); err == nil { return decode(s), nil // hit } else if err != redis.Nil { return User{}, err // ошибка Redis — решай: fail или fallback в БД } u, err := db.LoadUser(ctx, id) // miss → из БД if err != nil { return User{}, err } // TTL + небольшой jitter против синхронного истечения (stampede) ttl := 10*time.Minute + time.Duration(rand.Intn(120))*time.Second rdb.Set(ctx, key, encode(u), ttl) return u, nil } ``` **Стратегии записи:** - **Write-through** — пишем в кэш и в БД синхронно: кэш всегда свежий, но запись дороже. - **Write-back / write-behind** — пишем в кэш, в БД асинхронно: быстро, но риск потери при сбое и сложность. - **Cache-aside** — на запись инвалидируем (`DEL`) ключ, пусть следующее чтение перечитает. **Инвалидация** — «одна из двух трудных задач в CS». Подходы: TTL (просто, но stale до истечения), явный `DEL` на запись, event-driven (CDC/outbox → инвалидация), versioned keys (`user:42:v7`). Предпочитай `DEL` после коммита БД, а не «обновление кэша» (избегаешь записи устаревшего значения при гонке). **Thundering herd / cache stampede** — при истечении популярного ключа множество запросов одновременно идут в БД и пересчитывают одно и то же. Защиты: - **Mutex/lock на пересчёт**: первый берёт `SET NX` lock и считает, остальные ждут/возвращают stale. - **TTL jitter** — рандомизация времени жизни, чтобы ключи не истекали синхронно. - **Early recomputation (probabilistic early expiration)** — обновлять ключ до истечения с вероятностью, растущей у конца TTL. - **Stale-while-revalidate** — отдавать устаревшее значение, асинхронно обновляя. **Negative caching** — кэшировать «не найдено» (с коротким TTL), чтобы запросы несуществующих ключей не били в БД (cache penetration). #### Distributed locks Базовый паттерн: ``` SET lock:resource NX PX 30000 // взять, только если свободен, с TTL ... критическая секция ... // unlock — ТОЛЬКО Lua с проверкой токена (см. выше), нельзя DEL вслепую ``` Почему токен и Lua: если просто `DEL` — можно удалить чужой lock, взятый после истечения нашего TTL. Почему TTL обязателен: чтобы lock не остался навсегда при падении владельца. **Проблемы single-instance lock:** мастер с lock падает до репликации на реплику → после failover lock «потерян», двое держат его одновременно. Отсюда **Redlock** (lock на N независимых мастерах, нужно большинство). **Критика Redlock (Martin Kleppmann):** TTL-lock небезопасен при GC-паузах / stop-the-world / сетевых задержках — владелец может «проснуться» после истечения TTL и продолжить работу, думая, что держит lock. Решение для корректности — **fencing token** (монотонно растущий номер, проверяемый ресурсом-получателем). Antirez отвечал, что Redlock рассчитан на другую модель отказов. Вывод для senior: **для взаимного исключения «по производительности» (избежать дублей работы) — ок; для корректности (нельзя допустить двойного выполнения никогда) — Redis-lock недостаточен, нужен fencing token или линеаризуемый координатор (etcd/ZooKeeper/Consul).** #### Rate limiting **Fixed window** — `INCR key` + `EXPIRE`; просто, но всплеск на границе окон (до 2x лимита). **Sliding window log** — храним timestamps в ZSET, чистим старые, считаем оставшиеся: ```go var slidingWindow = redis.NewScript(` local key, now, window, limit = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]) redis.call("ZREMRANGEBYSCORE", key, 0, now - window) -- удалить устаревшие local count = redis.call("ZCARD", key) if count < limit then redis.call("ZADD", key, now, now .. ":" .. math.random()) -- уникальный member redis.call("PEXPIRE", key, window) return 1 -- allowed end return 0 -- rejected `) // allowed == 1? allowed, _ := slidingWindow.Run(ctx, rdb, []string{"rl:user:42"}, time.Now().UnixMilli(), int64(60000), 100).Int() _ = allowed ``` Точно, но память O(числа запросов в окне). **Token bucket** — хранить токены и время последнего пополнения (в Hash), пополнять лениво при запросе. Допускает burst до размера ведра, экономнее по памяти. Реализуется одним Lua-скриптом для атомарности. **Sliding window counter** — аппроксимация: взвешенная сумма текущего и предыдущего fixed-окна. Компромисс точности и памяти. Главное: любой rate limiter под конкуренцией должен быть **атомарным** → Lua-скрипт (read-modify-write в одной операции), иначе гонки превышают лимит. #### Очереди и Streams - **List-очередь**: `LPUSH` + `BRPOP`. Просто, но нет ack — если consumer упал после `BRPOP`, сообщение потеряно (паттерн reliable queue: `BLMOVE`/`LMOVE` в processing-список, удалять после обработки). - **Streams**: `XADD` + consumer group (`XGROUP CREATE`, `XREADGROUP`), `XACK` после успеха, `XPENDING`/`XCLAIM` для зависших. Поддержка нескольких групп (fan-out), replay по ID, ограничение длины `XADD ... MAXLEN ~ 10000`. Если нужны гарантии at-least-once и реальная очередь — Streams, не List. #### Репликация и Cluster - **Репликация**: один master, N replica, **асинхронная** по умолчанию (реплика может отставать → читаешь с реплики stale данные). `WAIT numreplicas timeout` блокирует, пока запись не дойдёт до N реплик, но это best-effort, не строгая durability. Failover — через **Redis Sentinel** (мониторинг + автопродвижение реплики). - **Redis Cluster**: данные шардируются по **16384 хеш-слотам** (`slot = CRC16(key) mod 16384`), каждый master владеет диапазоном слотов. Горизонтальное масштабирование и отказоустойчивость (replica на каждый master). Ограничение: **multi-key команды работают только если ключи в одном слоте** → **hash tags** `{user:42}:profile` и `{user:42}:cart` форсируют один слот. Транзакции/Lua с несколькими ключами — тоже в пределах слота. #### Eviction policies Когда `maxmemory` достигнут, Redis удаляет ключи по политике: | Политика | Поведение | |---|---| | `noeviction` | не удаляет, новые записи → ошибка (для primary-store) | | `allkeys-lru` | вытесняет наименее недавно использованные среди всех ключей | | `allkeys-lfu` | наименее **часто** используемые (Redis 4+, точнее для горячих данных) | | `volatile-lru` / `volatile-lfu` | то же, но только среди ключей с TTL | | `allkeys-random` / `volatile-random` | случайно | | `volatile-ttl` | сначала ключи с наименьшим оставшимся TTL | Redis LRU/LFU — **аппроксимация** (сэмплирование `maxmemory-samples`, не точный LRU), ради скорости. Для чистого кэша `allkeys-lru`/`allkeys-lfu`; если часть ключей — persistent, а часть — кэш, используй `volatile-*` + ставь TTL только на кэш-ключи. --- ## Подводные камни / gotchas - **`KEYS *` в проде** блокирует весь инстанс — только `SCAN`. Аналогично большие `HGETALL`/`SMEMBERS`/`ZRANGE 0 -1`. - **MULTI/EXEC ≠ ACID**: нет rollback, нет изоляции в смысле SQL; ошибка в одной команде не откатывает остальные. Для условной логики — `WATCH` (CAS) или Lua. - **Pipelining путают с транзакцией** — pipeline лишь экономит RTT, атомарности не даёт. - **Async-репликация → потеря подтверждённых записей** при failover; `WAIT` смягчает, но не устраняет. Не считай Redis линеаризуемым по умолчанию. - **Distributed lock без токена/Lua** удаляет чужой lock; без TTL — вечный lock; с TTL — риск двойного владения при паузах (нужен fencing token для корректности). - **Stampede при истечении горячего ключа** — без jitter/lock/early-expiry получишь шквал на БД. - **Big keys / hot keys**: огромный ключ (миллионы элементов) → блокировки и неравномерная нагрузка на шард; hot key в Cluster перегружает один узел. - **`DEL` большого ключа блокирует** — используй `UNLINK` (асинхронное освобождение памяти). - **Cluster и multi-key**: команды между слотами падают с `CROSSSLOT` — нужны hash tags. - **`EXPIRE` сбрасывается** некоторыми командами? Нет — но `SET` без `KEEPTTL` затирает TTL. В Redis 6+ есть `SET ... KEEPTTL`. - **`INCR` на нечисловой строке** → ошибка; счётчики держи отдельными ключами. - **`fork` для RDB/AOF-rewrite** при больших датасетах вызывает latency-спайки и риск OOM (copy-on-write + интенсивная запись удваивает память). - **Кэширование результата ошибки/частичных данных** — кэшируй только валидные ответы (или negative-cache «не найдено» с коротким TTL осознанно). - **Сериализация**: `GETSET` устарел (используй `SET ... GET`); следи за версией Redis при использовании новых флагов. --- ## Вопросы на собеседовании **В:** Redis однопоточный — как тогда он быстрый и как это влияет на консистентность? **О:** Команды исполняются в одном event-loop, поэтому каждая команда атомарна без локов и нет издержек синхронизации; всё in-memory + эффективные структуры данных. Узкое место — одно ядро и сеть, поэтому масштабируют шардированием (Cluster). I/O и фоновые задачи (RDB-fork, `UNLINK`) в новых версиях многопоточны, но логика команд сериализована. Минус — долгая команда (`KEYS *`, тяжёлый Lua) блокирует всех. **В:** В чём разница RDB и AOF, что выбрать для durability? **О:** RDB — периодические бинарные снапшоты: компактно, быстрый рестарт, но теряются данные между снапшотами. AOF — журнал команд: с `appendfsync everysec` потеря ≤1с, с `always` — почти ноль, ценой throughput; рестарт медленнее. Дефолт Redis 7 — гибрид (RDB-преамбула + AOF-хвост). Но даже AOF `always` не гарантирует durability при failover из-за асинхронной репликации. **В:** Объясни CAP и где Redis на этой карте. **О:** При сетевом разделении выбираешь C или A. Одиночный Redis — CP-ish на узле (консистентен, но недоступен при падении). Redis с async-репликацией ближе к AP при failover: жертвует консистентностью (можно потерять последние записи) ради доступности. По PACELC в нормальном режиме Redis выбирает latency. Строгой линеаризуемости из коробки нет. **В:** Как реализовать распределённый lock и какие у него проблемы? **О:** `SET key uniqueToken NX PX ttl`, unlock — Lua с проверкой токена. Проблемы: при падении master до репликации lock дублируется после failover; при GC-паузе владелец может пережить TTL и нарушить взаимное исключение. Redlock (N мастеров, большинство) повышает надёжность, но раскритикован Kleppmann: для **корректности** нужен fencing token, проверяемый самим ресурсом, либо линеаризуемый координатор (etcd/ZooKeeper). Для оптимизации «не делать работу дважды» Redis-lock достаточно. **В:** Что такое cache stampede / thundering herd и как бороться? **О:** При истечении популярного ключа лавина запросов одновременно пересчитывает его, перегружая БД. Защиты: lock на пересчёт (`SET NX`, остальные ждут/получают stale), TTL jitter (рассинхронизировать истечение), probabilistic early recomputation, stale-while-revalidate. Дополнительно negative caching против penetration. **В:** Cache-aside vs write-through vs write-back и инвалидация? **О:** Cache-aside: приложение читает из кэша, при miss грузит из БД и кладёт; на запись — инвалидирует (`DEL`). Write-through: синхронно в кэш и БД (свежесть ценой latency). Write-back: в кэш сразу, в БД асинхронно (быстро, риск потери). Инвалидацию предпочтительно делать `DEL` после коммита БД (не «обновлением», чтобы не записать stale при гонке); альтернативы — TTL, versioned keys, event/CDC-driven. **В:** Когда ZSET, когда HyperLogLog, когда Bitmap? **О:** ZSET — упорядоченные данные со score (leaderboard, sliding-window rate limit, приоритетная очередь), O(log N). Bitmap — булев признак на сущность по числовому id (DAU, флаги), считаешь `BITCOUNT`, очень компактно. HyperLogLog — приблизительный count-distinct (~0.81% ошибка, ~12 КБ независимо от объёма), когда точный Set был бы слишком большим. **В:** Как работает Redis Cluster и какие ограничения на операции? **О:** Данные распределены по 16384 хеш-слотам (`CRC16(key) mod 16384`), каждый master владеет диапазоном, реплики дают отказоустойчивость. Multi-key команды (включая транзакции и Lua с несколькими ключами) работают только если ключи в одном слоте — иначе `CROSSSLOT`. Чтобы связанные ключи попали в один слот, используют hash tags `{user:42}:...`. **В:** Как сделать корректный rate limiter на Redis? **О:** Варианты: fixed window (`INCR`+`EXPIRE`, но всплеск на границе), sliding window log (ZSET с timestamps — точно, но память O(N запросов)), token bucket (Hash с токенами и lazy refill — допускает burst, экономен), sliding window counter (аппроксимация). Ключевое — атомарность read-modify-write через Lua-скрипт, иначе под конкуренцией лимит превышается. **В:** MULTI/EXEC — это настоящая транзакция? **О:** Нет в ACID-смысле. Команды очередь, `EXEC` исполняет их атомарно и без вклинивания (single-threaded), но нет rollback при логической ошибке и нет уровней изоляции SQL. Для условной модификации — `WATCH` (оптимистичная блокировка/CAS) или Lua-скрипт (атомарный read-modify-write). --- ## На что копают на senior+ - **Durability vs availability trade-offs**: понимаешь ли, что async-репликация + AOF не равно «данные не потеряются», и когда Redis нельзя делать source of truth. Знание `WAIT`, его ограничений, и почему он не даёт строгой гарантии. - **Корректность distributed lock**: умеешь объяснить fencing token, разницу между «lock для производительности» и «lock для корректности», критику Redlock и когда нужен etcd/ZooKeeper вместо Redis. - **Консистентность как спектр**: read-after-write, monotonic reads, чтение с реплики и stale data; tunable consistency в Cassandra (`R+W>N`), read/write concern в Mongo, strongly consistent reads в DynamoDB — не сводишь NoSQL к «eventual». - **Access-pattern-driven моделирование** для Cassandra/DynamoDB (single-table design, денормализация, hot partition) — проектируешь от запросов, оцениваешь стоимость GSI и full scan. - **Операционные риски Redis**: big keys, hot keys, `fork`-latency и copy-on-write при RDB/rewrite, eviction под давлением памяти (LRU vs LFU, аппроксимация), блокирующие команды. - **Cluster-aware дизайн**: hash tags, CROSSSLOT, перешардирование (slot migration), почему multi-key атомарность ограничена. - **Стоимость и границы**: когда Postgres + JSONB/`UNLOGGED`-таблицы/`pg_partman` решают задачу без введения нового хранилища; чем платишь за каждый дополнительный datastore (операционная сложность, консистентность между ними, dual-write проблема → outbox/CDC). - **Защита кэша**: stampede, penetration (negative cache, bloom filter), avalanche (массовое одновременное истечение), и как их различать. - **Идемпотентность и at-least-once** в Streams: дедупликация на стороне consumer, обработка `XPENDING`/`XCLAIM`, сравнение с Kafka.