Skip to content

Latest commit

 

History

History
1680 lines (1222 loc) · 76.4 KB

File metadata and controls

1680 lines (1222 loc) · 76.4 KB

Лекция 25. NoSQL. Куки, сессии, кеш

Оглавление курса

Блок 1 — Python Basic (1–6)
Блок 2 — Git (7–8)
Блок 3 — Python Advanced (9–14)
Блок 4 — SQL (15–17)
Блок 5 — Django (19–26)
Блок 6 — Django Rest Framework (27–30)
Блок 7 — Python async (31–33)
Блок 8 — Deployment (34–35)

📚 Введение в NoSQL

🤔 Что такое NoSQL?

NoSQL (от англ. Not Only SQL — «не только SQL») — это термин, обозначающий типы баз данных, отличные от традиционных реляционных СУБД. NoSQL БД предлагают более гибкие модели хранения данных и лучше подходят для масштабируемых распределённых систем.

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

Подробнее:


📜 Краткая история

Год Событие
1970 Эдгар Кодд публикует реляционную модель данных
1995-2000 Бум интернета, рост нагрузок
2004 Google публикует статью о BigTable
2007 Amazon публикует статью о Dynamo
2009 Термин «NoSQL» становится популярным
2009 MongoDB, Redis, Cassandra выходят в open source
2010+ NoSQL становится мейнстримом

🧨 Почему появился NoSQL?

Реляционные базы данных (PostgreSQL, MySQL) отлично работают, но имеют ограничения:

Проблемы SQL при масштабировании

                    ┌─────────────┐
                    │   Клиенты   │
                    │  (миллионы) │
                    └──────┬──────┘
                           │
                           ▼
                    ┌──────────────┐
                    │  Один сервер │  ← Узкое место!
                    │  PostgreSQL  │
                    └──────────────┘

Вертикальное масштабирование (Scale Up):

  • Добавляем RAM, CPU, SSD
  • Дорого, есть физический предел
  • Единая точка отказа

Горизонтальное масштабирование (Scale Out):

  • Добавляем больше серверов
  • SQL плохо масштабируется горизонтально из-за JOIN и транзакций

Когда SQL становится проблемой

Проблема Пример
Жёсткая схема Добавление поля требует ALTER TABLE на миллионах строк
JOIN на больших данных Соединение 10 таблиц по 100 млн строк каждая
Транзакции между серверами Распределённые транзакции очень медленные
Неструктурированные данные Логи, события, JSON с разной структурой

⚖️ Подробно о CAP-теореме

CAP-теорема (также называемая теоремой Брюера) гласит, что в распределённой системе невозможно одновременно обеспечить:

Свойство Описание
Consistency (Согласованность) Все узлы видят одинаковые данные в один и тот же момент.
Availability (Доступность) Система всегда отвечает на запрос (даже если это не последняя версия).
Partition Tolerance (Устойчивость к разделению) Система продолжает работать, даже если между узлами есть сетевые проблемы.

🧠 Зачем нужна CAP-теорема? При проектировании распределённых систем (особенно в NoSQL) часто приходится жертвовать чем-то ради надёжности и масштабируемости. Например:

  • В случае сетевого разделения можно либо отказать в доступе (жертвуем A),
  • Либо отдать устаревшие данные (жертвуем C).

Вывод: система может выбрать максимум два из трёх свойств одновременно.

Примеры:

  • 🔸 Cassandra — AP: предпочитает доступность и устойчивость, согласованность достигается позже (eventual consistency).
  • 🔸 MongoDB (до 4.0) — CP: жертвует доступностью при проблемах с сетью.
  • 🔸 Redis (в кластере) — чаще AP.

🔢 Основные типы NoSQL баз данных

Существует 4 основных типа NoSQL баз данных, каждый оптимизирован под свои сценарии:


1. 🗝️ Key-Value (Ключ-значение)

Простейшая модель, напоминающая словарь Python: ключ → значение.

┌─────────────────┬──────────────────────────────┐
│      Ключ       │          Значение            │
├─────────────────┼──────────────────────────────┤
│ user:123        │ {"name": "Alice", "age": 25} │
│ session:abc123  │ {"user_id": 123, "cart": []} │
│ cache:homepage  │ "<html>...</html>"           │
│ counter:visits  │ 1542367                      │
└─────────────────┴──────────────────────────────┘

Характеристики:

  • ⚡ Очень быстрый доступ O(1)
  • 🔑 Поиск только по ключу (нет запросов по значению)
  • 📦 Значение — «чёрный ящик» (строка, JSON, бинарные данные)

Популярные решения:

БД Особенности Когда использовать
Redis In-memory, структуры данных, Pub/Sub Кеш, сессии, очереди, real-time
Memcached Только кеш, очень простой Простое кеширование
Amazon DynamoDB Managed, масштабируемый AWS-проекты, serverless
etcd Распределённый, консистентный Конфигурации, service discovery

Реальные примеры использования:

  • GitHub — Redis для очередей задач и кеша
  • Twitter — Redis для timeline и счётчиков
  • Pinterest — Redis для рекомендаций

Пример паттерна именования ключей:

# Хорошие ключи — структурированные, понятные
"user:123:profile"           # профиль пользователя 123
"user:123:sessions"          # сессии пользователя
"article:456:views"          # счётчик просмотров статьи
"cache:homepage:v2"          # кеш главной страницы, версия 2
"ratelimit:ip:192.168.1.1"   # rate limiting по IP

# Плохие ключи
"u123"                       # непонятно что это
"data"                       # слишком общее
"temp_value_for_user"        # нет структуры

2. 📄 Document-oriented (Документные БД)

Хранят данные в виде документов (JSON, BSON). Каждый документ — самодостаточная единица с произвольной структурой.

Сравнение с SQL:

SQL (PostgreSQL):                    Document (MongoDB):
┌─────────────────────────┐          ┌─────────────────────────────────┐
│ users                   │          │ {                               │
├────┬───────┬────────────┤          │   "_id": "user_001",            │
│ id │ name  │ email      │          │   "name": "Alice",              │
├────┼───────┼────────────┤          │   "email": "alice@example.com", │
│ 1  │ Alice │ alice@...  │          │   "orders": [                   │
└────┴───────┴────────────┘          │     {"item": "Book", "qty": 2}, │
┌─────────────────────────┐          │     {"item": "Pen", "qty": 5}   │
│ orders                  │          │   ],                            │
├────┬─────────┬──────────┤          │   "address": {                  │
│ id │ user_id │ item     │          │     "city": "Moscow",           │
├────┼─────────┼──────────┤          │     "zip": "123456"             │
│ 1  │ 1       │ Book     │          │   }                             │
│ 2  │ 1       │ Pen      │          │ }                               │
└────┴─────────┴──────────┘          └─────────────────────────────────┘
     ↑ Нужен JOIN!                        ↑ Всё в одном документе!

Характеристики:

  • 📝 Гибкая схема — разные документы могут иметь разные поля
  • 🔍 Богатые запросы — можно искать по любому полю
  • 📦 Вложенные структуры — массивы, объекты внутри документов
  • 🚀 Нет JOIN — данные денормализованы

Популярные решения:

БД Особенности Когда использовать
MongoDB Самая популярная, богатый функционал Общего назначения, прототипы
CouchDB HTTP API, offline-first Мобильные приложения, синхронизация
Firestore Real-time, managed Firebase-проекты, мобильные
Amazon DocumentDB MongoDB-совместимый AWS-проекты

Когда документная БД лучше SQL:

Сценарий Почему документная БД лучше
CMS, блоги Статьи имеют разную структуру (видео, галерея, текст)
Каталог товаров У телефона и футболки разные атрибуты
Логи, события Структура может меняться
Прототипирование Не нужно думать о схеме заранее

Когда SQL лучше:

Сценарий Почему SQL лучше
Финансы, банкинг Нужны ACID-транзакции
Сложные отчёты Нужны JOIN и агрегации
Связанные данные Много связей many-to-many

3. 📊 Column-family (Колонночные БД)

Колонночные базы данных на первый взгляд похожи на реляционные, но различие — в физической организации хранения данных.

🆚 В чём отличие от реляционных БД?
Row-oriented (PostgreSQL):           Column-oriented (Cassandra):
┌────┬───────┬─────┬────────┐        ┌─────────────────────────────┐
│ id │ name  │ age │ city   │        │ id:    [1, 2, 3, 4, 5...]   │
├────┼───────┼─────┼────────┤        │ name:  [Alice, Bob, ...]    │
│ 1  │ Alice │ 30  │ Moscow │        │ age:   [30, 25, 28, ...]    │
│ 2  │ Bob   │ 25  │ Prague │        │ city:  [Moscow, Prague, ...]│
│ 3  │ Carol │ 28  │ Berlin │        └─────────────────────────────┘
└────┴───────┴─────┴────────┘
     ↑ Строки хранятся вместе             ↑ Столбцы хранятся вместе
Реляционные БД (PostgreSQL, MySQL) Колонночные БД (Cassandra, Bigtable)
Единица хранения строка (row) столбец (column) или «семейство столбцов»
Физическое хранение строки лежат вместе значения одного столбца лежат вместе
Сценарий оптимизации запись/чтение строк аналитика, агрегации по столбцам
Гибкость схемы строго задана допускаются разные наборы столбцов

Почему это важно?

-- Этот запрос БЫСТРЕЕ в колонночной БД:
SELECT AVG(age) FROM users WHERE city = 'Moscow';
-- Читаем только 2 столбца (age, city), а не все данные

-- Этот запрос БЫСТРЕЕ в row-oriented БД:
SELECT * FROM users WHERE id = 123;
-- Читаем одну строку целиком

Популярные решения:

БД Особенности Когда использовать
Apache Cassandra Распределённая, высокая доступность Логи, IoT, временные ряды
Google Bigtable Managed, петабайты данных Big Data, ML
ClickHouse Аналитика, очень быстрый OLAP, дашборды
Apache HBase На базе Hadoop Big Data экосистема

Реальные примеры:

  • Netflix — Cassandra для 10+ петабайт данных о просмотрах
  • Discord — Cassandra для миллиардов сообщений
  • Uber — Cassandra для геолокации и логов

4. 🔗 Graph (Графовые БД)

Графовые базы данных хранят данные как узлы (nodes) и связи (edges) между ними.

        ┌─────────┐
        │  Alice  │
        └────┬────┘
             │ FRIEND
             ▼
        ┌─────────┐         ┌─────────┐
        │   Bob   │─WORKS_AT─▶│ Google  │
        └────┬────┘         └─────────┘
             │ LIKES
             ▼
        ┌─────────┐
        │  Python │
        └─────────┘

Язык запросов Cypher (Neo4j):

// Найти друзей друзей Alice
MATCH (alice:Person {name: 'Alice'})-[:FRIEND]->()-[:FRIEND]->(fof)
RETURN fof.name

// Найти кратчайший путь между двумя людьми
MATCH path = shortestPath(
  (a:Person {name: 'Alice'})-[:FRIEND*]-(b:Person {name: 'Charlie'})
)
RETURN path

// Рекомендации: "Люди, которые лайкнули то же, что и вы"
MATCH (me:Person {name: 'Alice'})-[:LIKES]->(thing)<-[:LIKES]-(other)
WHERE me <> other
RETURN other.name, COUNT(thing) as common_interests
ORDER BY common_interests DESC
🧠 Почему графы так важны?
Задача SQL подход Graph подход
Друзья друзей 2 JOIN 1 простой запрос
6 рукопожатий 6 JOIN (очень медленно) Один запрос с глубиной 6
Кратчайший путь Рекурсивные CTE Встроенный алгоритм

Популярные решения:

БД Особенности Когда использовать
Neo4j Самая популярная, Cypher Социальные сети, рекомендации
Amazon Neptune Managed, AWS AWS-проекты
ArangoDB Multi-model (graph + document) Гибридные сценарии
JanusGraph Распределённая, open source Big Data графы

Реальные примеры:

  • LinkedIn — граф профессиональных связей (800M+ узлов)
  • Airbnb — граф доверия для предотвращения мошенничества
  • eBay — рекомендации на основе графа покупок

💡 Сводная таблица: какую NoSQL выбрать?

Сценарий Тип NoSQL Примеры БД Почему
Кеш, сессии Key-Value Redis, Memcached O(1) доступ, TTL
Очереди задач Key-Value Redis, RabbitMQ Pub/Sub, списки
CMS, каталог товаров Document MongoDB Гибкая схема
Логи, события Document / Column MongoDB, Cassandra Append-only, масштабирование
Аналитика, отчёты Column-family ClickHouse, Cassandra Агрегации по столбцам
Временные ряды (IoT) Column-family InfluxDB, TimescaleDB Оптимизация для time-series
Социальные сети Graph Neo4j Связи между пользователями
Рекомендации Graph Neo4j, Neptune «Похожие товары»
Fraud detection Graph Neo4j Поиск подозрительных паттернов

🆚 SQL vs NoSQL: детальное сравнение

Критерий SQL (PostgreSQL, MySQL) NoSQL (MongoDB, Redis)
Модель данных Таблицы, строки, столбцы Документы, ключ-значение, графы
Схема Строгая, заранее определённая Гибкая, может меняться
Язык запросов SQL (стандартизирован) Разный для каждой БД
Транзакции ACID (полная поддержка) Часто BASE (eventual consistency)
Связи JOIN, внешние ключи Денормализация, вложенные документы
Масштабирование Вертикальное (сложно горизонтально) Горизонтальное (шардинг)
Консистентность Строгая Часто eventual
Зрелость 40+ лет, проверено временем 15+ лет, быстро развивается

🔬 ACID vs BASE

ACID (SQL)

Свойство Описание
Atomicity Транзакция выполняется полностью или не выполняется вообще
Consistency БД всегда в согласованном состоянии
Isolation Параллельные транзакции не влияют друг на друга
Durability Завершённые транзакции сохраняются даже при сбое
# ACID пример: перевод денег
with transaction.atomic():
    sender.balance -= 100
    sender.save()
    receiver.balance += 100
    receiver.save()
    # Если здесь ошибка — откатится ВСЁ

BASE (NoSQL)

Свойство Описание
Basically Available Система всегда отвечает (возможно, устаревшими данными)
Soft state Состояние может меняться со временем без внешнего воздействия
Eventual consistency Данные станут согласованными... когда-нибудь
# BASE пример: счётчик лайков
redis.incr("post:123:likes")  # Мгновенно
# Реплики получат обновление через несколько миллисекунд
# Пользователь может увидеть 999 лайков вместо 1000 — это ОК

🏗️ Polyglot Persistence: используем разные БД вместе

В реальных проектах часто используют несколько баз данных, каждую для своей задачи:

┌─────────────────────────────────────────────────────────────┐
│                      Веб-приложение                         │
└─────────────────────────────────────────────────────────────┘
         │              │              │              │
         ▼              ▼              ▼              ▼
    ┌──────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
    │PostgreSQL│    │  Redis  │    │ MongoDB │    │  Neo4j  │
    │          │    │         │    │         │    │         │
    │ Заказы   │    │ Кеш     │    │  Логи   │    │ Рекомен-│
    │ Платежи  │    │ Сессии  │    │ События │    │ дации   │
    │ Юзеры    │    │ Очереди │    │         │    │         │
    └──────────┘    └─────────┘    └─────────┘    └─────────┘

Пример архитектуры интернет-магазина:

Данные БД Причина
Пользователи, заказы, платежи PostgreSQL ACID, транзакции
Сессии, корзина Redis Скорость, TTL
Каталог товаров MongoDB Разные атрибуты у товаров
Поисковый индекс Elasticsearch Полнотекстовый поиск
Рекомендации Neo4j «С этим товаром покупают»
Аналитика ClickHouse Быстрые агрегации

🧾 Когда использовать NoSQL?

Используй NoSQL, если:

Критерий Пример
Гибкая/меняющаяся схема Каталог товаров с разными атрибутами
Горизонтальное масштабирование Миллионы пользователей, петабайты данных
Высокая скорость записи Логи, события, IoT-данные
Кеширование Сессии, временные данные
Связи между сущностями Социальные сети, рекомендации
Eventual consistency допустима Счётчики лайков, просмотров

Оставайся на SQL, если:

Критерий Пример
Нужны ACID-транзакции Финансовые операции, платежи
Сложные JOIN и отчёты Бухгалтерия, ERP-системы
Строгая схема данных Регулируемые отрасли (медицина, финансы)
Небольшой проект Стартап на ранней стадии
Команда знает только SQL Не усложняй без необходимости

⚠️ Типичные ошибки при выборе NoSQL

1. «MongoDB — это круто, давайте всё на ней!»

# ❌ Плохо: финансовые транзакции в MongoDB
def transfer_money(from_user, to_user, amount):
    # Нет гарантии атомарности между документами!
    db.users.update_one({"_id": from_user}, {"$inc": {"balance": -amount}})
    # Что если здесь произойдёт сбой?
    db.users.update_one({"_id": to_user}, {"$inc": {"balance": amount}})

Решение: Используй PostgreSQL для финансов.

2. «Redis — это база данных, храним там всё!»

# ❌ Плохо: Redis как основное хранилище
redis.set("user:123", json.dumps(user_data))
# Что если Redis перезапустится? Данные потеряны!

Решение: Redis — для кеша и временных данных. Основные данные — в PostgreSQL.

3. «Нам не нужны JOIN, денормализуем всё!»

# ❌ Плохо: дублирование данных везде
{
    "_id": "order_1",
    "user": {"name": "Alice", "email": "alice@example.com"},  # Копия!
    "items": [...]
}
# Если Alice сменит email — нужно обновить ВСЕ заказы!

Решение: Денормализуй только то, что читается часто и меняется редко.

4. «Схема не нужна, это же NoSQL!»

# ❌ Плохо: хаос в структуре документов
{"name": "Alice", "age": 25}
{"userName": "Bob", "years_old": "thirty"}  # Разные поля, разные типы!

Решение: Используй валидацию схемы (JSON Schema в MongoDB, Pydantic в Python).


🎯 Чек-лист выбора базы данных

1. Нужны ли ACID-транзакции?
   ├─ Да → PostgreSQL / MySQL
   └─ Нет → продолжаем

2. Какой тип данных?
   ├─ Табличные, связанные → PostgreSQL
   ├─ Документы (JSON) → MongoDB
   ├─ Ключ-значение → Redis
   ├─ Временные ряды → InfluxDB / TimescaleDB
   ├─ Графы → Neo4j
   └─ Логи, события → Elasticsearch / ClickHouse

3. Какой масштаб?
   ├─ < 1M записей → PostgreSQL справится
   ├─ 1M – 100M → PostgreSQL с оптимизацией или NoSQL
   └─ > 100M → Скорее всего, нужен NoSQL или шардинг

4. Какая нагрузка?
   ├─ Больше чтения → Кеш (Redis) + любая БД
   ├─ Больше записи → Cassandra, MongoDB
   └─ Аналитика → ClickHouse, BigQuery

💬 Комментарий преподавателя: Redis — универсальный инструмент. Он почти всегда есть в стеке. Даже если не используется на старте, его часто добавляют позже для кэширования, очередей и хранения сессий. Начните с PostgreSQL + Redis — это покрывает 90% задач.


🔴 Практика: работа с Redis напрямую

Хотя Django предоставляет абстракции для кеша и сессий, иногда полезно работать с Redis напрямую через redis-py.

Установка

pip install redis

Запуск Redis

Без запуска Redis работать не будет!

redis-server

Базовые операции

import redis

# Подключение
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# Строки (самый простой тип)
r.set("user:1:name", "Alice")
r.get("user:1:name")  # "Alice"
r.set("user:1:name", "Alice", ex=3600)  # с TTL 1 час

# Счётчики
r.incr("page:views")  # атомарный инкремент
r.incrby("page:views", 10)

# Хеши (как словари)
r.hset("user:1", mapping={"name": "Alice", "email": "alice@example.com"})
r.hget("user:1", "name")  # "Alice"
r.hgetall("user:1")  # {"name": "Alice", "email": "..."}

# Списки (очереди)
r.lpush("queue:emails", "email1", "email2")
r.rpop("queue:emails")  # "email1" (FIFO)

# Множества
r.sadd("article:1:tags", "python", "django", "redis")
r.smembers("article:1:tags")  # {"python", "django", "redis"}

# Sorted Sets (для рейтингов, лидербордов)
r.zadd("leaderboard", {"alice": 100, "bob": 85, "charlie": 92})
r.zrevrange("leaderboard", 0, 2, withscores=True)  # топ-3

# TTL и удаление
r.expire("user:1:name", 300)  # установить TTL
r.ttl("user:1:name")  # оставшееся время
r.delete("user:1:name")

🍪 Куки, 🧾 Сессии и ⚡ Кеширование в Django

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


🧭 Что нужно понимать

  • HTTP stateless — каждый запрос сам по себе, без истории.
  • Cookie — способ сохранять небольшие данные на стороне клиента (браузера).
  • Session — способ сохранять пользовательские данные на сервере, привязанные к уникальному sessionid, передаваемому в Cookie.
  • Cache — быстрая память для хранения часто используемых данных (не обязательно связанных с пользователем).

🍪 Cookie (печеньки)

Куки — это пары ключ:значение, которые браузер хранит локально и отправляет на сервер с каждым HTTP-запросом к соответствующему домену.

Примеры применения:

  • Сохранение корзины товаров
  • Запоминание темы оформления
  • Предзаполнение форм
  • Авторизация через sessionid

Куки существуют только на уровне HTTP запроса! Их может запоминать браузер, но прямого отношения к серверу они не имеют!

Пример установки/удаления cookie с флагами безопасности:

from django.http import HttpResponse

resp = HttpResponse("ok")
resp.set_cookie(
    "theme", "dark", max_age=7*24*3600,
    secure=True, httponly=True, samesite="Lax"
)
# ...
resp.delete_cookie("theme")

🔒 Флаги безопасности cookies

Флаг Описание Рекомендация
secure=True Cookie передаётся только по HTTPS ✅ Всегда в production
httponly=True Cookie недоступна из JavaScript (защита от XSS) ✅ Для sessionid, токенов
samesite="Strict" Cookie отправляется только с того же сайта Максимальная защита от CSRF
samesite="Lax" Cookie отправляется при навигации, но не при POST с других сайтов ✅ Рекомендуемый баланс
samesite="None" Cookie отправляется всегда (требует secure=True) Только для cross-site сценариев
max_age Время жизни в секундах Зависит от сценария
expires Дата истечения (альтернатива max_age) Используйте max_age

⚠️ Важно: Django по умолчанию устанавливает SESSION_COOKIE_HTTPONLY = True и SESSION_COOKIE_SAMESITE = "Lax".


🔐 Signed Cookies (подписанные куки)

Обычные cookies можно подделать на клиенте. Signed cookies защищены криптографической подписью — Django проверяет, что значение не было изменено.

# Установка подписанной cookie
response.set_signed_cookie(
    "user_preference",
    "dark_mode",
    salt="user-prefs",  # дополнительная защита
    max_age=3600
)

# Чтение подписанной cookie
try:
    value = request.get_signed_cookie(
        "user_preference",
        salt="user-prefs",
        max_age=3600  # проверка срока действия
    )
except (KeyError, signing.BadSignature):
    value = None  # cookie отсутствует или подделана

Когда использовать:

  • Хранение предпочтений пользователя
  • Данные, которые не должны быть изменены клиентом
  • Альтернатива сессиям для небольших данных

📌 Signed cookies используют SECRET_KEY — при его смене все подписи станут невалидными.


🧾 Сессии

Документация: https://docs.djangoproject.com/en/stable/topics/http/sessions/ — основы сессий; настройки: https://docs.djangoproject.com/en/stable/ref/settings/#sessions

Сессия — это механизм сохранения информации между запросами одного и того же пользователя. Чаще всего для реализации сессий используется привязка к куки.

Как это работает в Django:

Браузер → POST /login → Сервер:
    - создаёт Session в БД
    - возвращает Set-Cookie: sessionid=<uuid>

Браузер → GET /profile → Cookie: sessionid → Сервер:
    - достаёт данные из request.session

Где хранятся данные:

Django поддерживает несколько backends для хранения сессий через настройку SESSION_ENGINE:

Backend SESSION_ENGINE Плюсы Минусы
Database (по умолчанию) django.contrib.sessions.backends.db Надёжно, просто Нагрузка на БД
Cache django.contrib.sessions.backends.cache Быстро Данные теряются при перезапуске
Cached DB django.contrib.sessions.backends.cached_db Быстро + надёжно Сложнее настройка
File django.contrib.sessions.backends.file Просто Медленно, проблемы с масштабированием
Signed Cookie django.contrib.sessions.backends.signed_cookies Без хранилища Ограничение 4KB, данные видны клиенту

Пример: сессии в Redis

# settings.py

# 1. Настраиваем кеш (Redis)
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
    }
}

# 2. Используем кеш для сессий
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

# Или cached_db для надёжности (кеш + БД как fallback)
# SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"

💡 Рекомендация: Для production используйте cached_db — это даёт скорость Redis и надёжность БД.

Предпосылки настройки (обычно включены в типичном проекте):

  • 'django.contrib.sessions' в INSTALLED_APPS
  • 'django.contrib.sessions.middleware.SessionMiddleware' в MIDDLEWARE
  • (Рекомендуется) 'django.contrib.auth.middleware.AuthenticationMiddleware'

🛠 Использование

# Работа с сессией в Django
request.session['visited'] = True

# Проверка
if request.session.get('visited'):
    ...

# Удаление одного ключа
del request.session['visited']

# Полная очистка сессии (удаляет все данные и создаёт новый sessionid)
request.session.flush()

# Очистка данных без смены sessionid
request.session.clear()

💡 flush() рекомендуется при logout — это предотвращает session fixation атаки.

💡 Пример — ограничение комментариев

def post_comment(request, comment):
    if request.session.get('has_commented', False):
        return HttpResponse("You've already commented.")
    request.session['has_commented'] = True
    return HttpResponse("Thanks!")

🧪 Пример — хранение временной метки

# ❌ Неправильно — datetime не сериализуется в JSON
# request.session['last_action'] = timezone.now()

# ✅ Правильно — сохраняем как ISO-строку
from django.utils import timezone

request.session['last_action'] = timezone.now().isoformat()

# При чтении преобразуем обратно
from datetime import datetime
last_action_str = request.session.get('last_action')
if last_action_str:
    last_action = datetime.fromisoformat(last_action_str)

⚠️ Сессии сериализуются в JSON, поэтому можно хранить только: str, int, float, bool, list, dict, None.

⏳ Срок жизни сессии

# Установить время жизни текущей сессии (секунды)
request.session.set_expiry(3600)  # 1 час

🧩 Вне запроса (SessionStore)

from django.contrib.sessions.backends.db import SessionStore

s = SessionStore()
s['foo'] = 'bar'
s.create()
s.session_key  # сохранённый ключ

🛑 Важные нюансы

  • Данные сессии сериализуются (по умолчанию JSON), поэтому объекты должны быть сериализуемыми. Подробно разберём сериализацию далее по курсу.
  • Данные сохраняются только при изменении request.session как объекта
request.session['foo'] = 'bar'  # сохранится
request.session['foo']['x'] = 1  # ❌ не сохранится

При вложенной мутации пометьте сессию изменённой:

request.session.setdefault("cart", {})["id123"] = 2
request.session.modified = True  # зафиксировать изменения для сохранения

✅ Можно настроить:

SESSION_SAVE_EVERY_REQUEST = True

🧹 Очистка старых сессий

python manage.py clearsessions

Команду рекомендуется запускать периодически (cron / periodic job) — подробнее о планировании задач разберём позже по курсу.

Или вручную:

from django.contrib.sessions.models import Session

Session.objects.filter(...).delete()

⚡ Кеширование

Документация: https://docs.djangoproject.com/en/stable/topics/cache/ — основы кеширования; настройки: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CACHES; per-view cache: https://docs.djangoproject.com/en/stable/topics/cache/#the-per-view-cache

🤔 Что такое кеш?

Кеш (cache) — это промежуточное хранилище данных, которое позволяет быстро получить результат без повторного выполнения дорогостоящей операции.

Представьте: вы каждый раз открываете холодильник, чтобы посмотреть, есть ли молоко. Это занимает время. Но если вы запомните, что молоко есть, — вам не нужно открывать холодильник снова. Это и есть кеширование.

В контексте веб-приложений кеш помогает избежать:

  • Повторных запросов к базе данных
  • Повторных вычислений (агрегации, сортировки)
  • Повторных запросов к внешним API
  • Повторного рендеринга шаблонов


⏱️ Почему кеш быстрее?

Операция Примерное время
Чтение из RAM (Redis) ~0.1 мс
Чтение из SSD ~0.1-1 мс
Запрос к PostgreSQL (простой) ~1-10 мс
Запрос к PostgreSQL (сложный JOIN) ~50-500 мс
Запрос к внешнему API ~100-1000 мс

Разница может быть в 100–1000 раз! Если главная страница делает 10 запросов к БД по 50 мс каждый — это 500 мс. С кешем — 1 мс.


🎯 Что кешировать?

Хорошие кандидаты для кеширования:

  • Данные, которые читаются часто, но меняются редко (список категорий, настройки)
  • Результаты тяжёлых вычислений (статистика, отчёты)
  • Ответы внешних API (курсы валют, погода)
  • Отрендеренные фрагменты HTML (сайдбар, меню)
  • Сериализованные данные для API

Плохие кандидаты:

  • Данные, которые меняются при каждом запросе
  • Персонализированные данные (если много пользователей)
  • Данные, где критична актуальность (баланс счёта)

📊 Ключевые метрики кеша

  • Hit Rate — процент запросов, которые нашли данные в кеше. Цель: >90%
  • Miss Rate — процент промахов (данных нет в кеше)
  • TTL (Time To Live) — время жизни записи в кеше
  • Eviction — вытеснение старых данных при нехватке памяти
Hit Rate = Cache Hits / (Cache Hits + Cache Misses) × 100%

💡 Если Hit Rate низкий — возможно, TTL слишком короткий или ключи генерируются неправильно.


📚 Стратегии кеширования

Существует несколько паттернов работы с кешем. Выбор зависит от требований к консистентности данных и нагрузки.

1️⃣ Cache-Aside (Lazy Loading)

Самая популярная стратегия. Приложение само управляет кешем.

Чтение:
1. Проверяем кеш
2. Если есть (cache hit) → возвращаем
3. Если нет (cache miss) → читаем из БД → сохраняем в кеш → возвращаем

Запись:
1. Пишем в БД
2. Инвалидируем (удаляем) кеш
def get_article(article_id):
    key = f"article:{article_id}"
    article = cache.get(key)

    if article is None:  # cache miss
        article = Article.objects.get(pk=article_id)
        cache.set(key, article, timeout=3600)

    return article

def update_article(article_id, data):
    article = Article.objects.get(pk=article_id)
    article.title = data['title']
    article.save()

    # Инвалидируем кеш
    cache.delete(f"article:{article_id}")

Плюсы: Простота, кешируются только нужные данные ❌ Минусы: Первый запрос всегда медленный (cold start)


2️⃣ Write-Through

Запись идёт через кеш. Данные всегда синхронизированы.

Запись:
1. Пишем в кеш
2. Кеш синхронно пишет в БД

Чтение:
1. Всегда читаем из кеша
def save_article(article):
    article.save()  # БД
    cache.set(f"article:{article.id}", article, timeout=3600)  # Кеш

Плюсы: Данные всегда актуальны, нет cache miss после записи ❌ Минусы: Запись медленнее (2 операции), кешируются даже редко читаемые данные


3️⃣ Write-Behind (Write-Back)

Асинхронная запись в БД. Сначала пишем в кеш, потом фоново в БД.

Запись:
1. Пишем в кеш
2. Асинхронно (через очередь) пишем в БД

Чтение:
1. Читаем из кеша
def increment_views(article_id):
    # Быстро инкрементируем в Redis
    cache.incr(f"article:{article_id}:views")

    # Периодически синхронизируем с БД (через Celery task)
    sync_views_to_db.delay(article_id)

Плюсы: Очень быстрая запись ❌ Минусы: Риск потери данных при сбое, сложность реализации


4️⃣ Read-Through

Кеш сам загружает данные. Приложение работает только с кешем.

Чтение:
1. Запрашиваем у кеша
2. Кеш сам загружает из БД при miss

В Django это можно реализовать через get_or_set:

def get_article(article_id):
    return cache.get_or_set(
        f"article:{article_id}",
        lambda: Article.objects.get(pk=article_id),
        timeout=3600
    )

🎯 Когда какую стратегию использовать?

Сценарий Рекомендуемая стратегия
Чтение >> Запись (блог, каталог) Cache-Aside
Критичная консистентность Write-Through
Высокая нагрузка на запись (счётчики, логи) Write-Behind
Простота кода важнее всего Read-Through (get_or_set)

⚠️ Проблемы кеширования

Cache Stampede (Thundering Herd)

Проблема: Кеш истёк, 1000 запросов одновременно идут в БД.

Решение: Блокировка или вероятностное обновление:

import random

def get_with_early_refresh(key, fetch_func, timeout=3600):
    data, expires_at = cache.get(key, (None, 0))

    # Вероятностное раннее обновление
    if data and time.time() < expires_at:
        # 5% шанс обновить заранее
        if random.random() < 0.05 and time.time() > expires_at - 60:
            data = fetch_func()
            cache.set(key, (data, time.time() + timeout), timeout)
        return data

    # Cache miss — обновляем
    data = fetch_func()
    cache.set(key, (data, time.time() + timeout), timeout)
    return data

Stale Data (устаревшие данные)

Проблема: Данные изменились, но в кеше старая версия.

Решения:

  • Короткий TTL
  • Инвалидация через сигналы
  • Версионирование ключей

💡 Пример

# Вместо:
Article.objects.filter(published=True).order_by('-date')[:5]

# Лучше:
articles = cache.get('homepage_articles')
if not articles:
    articles = Article.objects.filter(...).all()
    cache.set('homepage_articles', articles, timeout=3600)

Альтернатива короче:

articles = cache.get_or_set(
    "homepage:articles:v1",
    lambda: Article.objects.filter(published=True).order_by("-date")[:5],
    timeout=3600,
)

Счётчики (зависят от backend, поддерживаются Redis/Memcached):

cache.incr("counter:visits", ignore_key_check=True)
# cache.decr("counter:visits")

📦 Варианты backends

Хранилище Плюсы Минусы
Redis Быстро, работает в памяти, масштабируется Требует установки и сервера
Memcached Очень быстрый, простой Только строковые ключи
FileBased Не требует серверов Медленный на больших объёмах
Database Ничего не нужно настраивать Нагрузка на БД
DummyCache Ничего не делает (для dev) Нет кеша :)

⚙️ Настройки кеша (пример Redis)

Установка backend:

python -m pip install django-redis
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
        "TIMEOUT": 3600,
        "KEY_PREFIX": "myapp",
    }
}

🎯 Использование

from django.core.cache import cache

cache.set("key", "value", timeout=60)
cache.get("key")  # "value"
cache.delete("key")

Массовые операции:

cache.set_many({"a": 1, "b": 2})
cache.get_many(["a", "b"])
cache.delete_many(["a", "b"])

🔢 Версионирование кеша

Версии позволяют инвалидировать кеш без удаления ключей — полезно при деплое:

# Установка с версией
cache.set("articles", data, version=1)

# Чтение конкретной версии
cache.get("articles", version=1)

# Инкремент версии (инвалидирует старый кеш)
cache.incr_version("articles")

# Глобальная версия в settings.py
CACHES = {
    "default": {
        # ...
        "VERSION": 1,  # увеличьте при деплое для сброса всего кеша
    }
}

🔄 Инвалидация кеша — самая сложная проблема

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

Инвалидация кеша — это процесс удаления или обновления устаревших данных в кеше. Звучит просто, но на практике это одна из самых коварных проблем в программировании.


❓ Почему это сложно?

  1. Связанные данные. Изменение одной сущности может влиять на множество кешей:

    • Изменили статью → нужно обновить: кеш статьи, кеш списка статей, кеш категории, кеш тегов, кеш RSS, кеш sitemap...
  2. Распределённые системы. Кеш может быть на нескольких серверах, и они должны синхронизироваться.

  3. Race conditions. Между чтением из БД и записью в кеш данные могут измениться.

  4. Каскадные зависимости. Изменение автора → все его статьи → все комментарии к статьям...


🛠️ Стратегии инвалидации

1. TTL-based (Time To Live)

Самый простой подход — данные автоматически устаревают через заданное время.

cache.set("articles:list", articles, timeout=300)  # 5 минут

Плюсы: Простота, гарантированное обновление ❌ Минусы: Данные могут быть устаревшими до истечения TTL

Когда использовать:

  • Данные, где небольшая задержка допустима (новости, каталог)
  • Внешние API (курсы валют — обновляются раз в час)

2. Event-based (через сигналы)

Явная инвалидация при изменении данных.

# blog/signals.py
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import Article


@receiver([post_save, post_delete], sender=Article)
def invalidate_article_cache(sender, instance, **kwargs):
    """Сбрасываем кеш при изменении статьи."""
    # 1. Кеш конкретной статьи
    cache.delete(f"article:{instance.pk}")

    # 2. Кеш списка статей (может быть несколько)
    cache.delete("homepage:articles")
    cache.delete(f"category:{instance.category_id}:articles")

    # 3. Кеш автора
    cache.delete(f"author:{instance.author_id}:articles")

Плюсы: Мгновенное обновление ❌ Минусы: Нужно знать все связанные ключи, легко что-то забыть

📌 Подробнее о сигналах — в лекции 26.


3. Версионирование ключей

Вместо удаления — меняем версию, старые ключи просто игнорируются.

# Глобальная версия для всех статей
ARTICLES_VERSION = cache.get("articles:version", 1)

def get_articles():
    key = f"articles:list:v{ARTICLES_VERSION}"
    return cache.get_or_set(key, fetch_articles, timeout=3600)

def invalidate_articles():
    # Инкрементируем версию — все старые ключи становятся "мусором"
    cache.incr("articles:version")

Плюсы: Атомарность, нет race conditions ❌ Минусы: Старые данные занимают память до eviction


4. Tag-based инвалидация

Группировка ключей по тегам — можно инвалидировать всё, связанное с тегом.

# Псевдокод (требует специальной реализации или библиотеки)
cache.set("article:1", data, tags=["articles", "category:5", "author:3"])
cache.set("article:2", data, tags=["articles", "category:5", "author:7"])

# Инвалидируем всё в категории 5
cache.invalidate_tag("category:5")  # удалит article:1 и article:2

💡 Django из коробки не поддерживает теги. Используйте django-cacheops или реализуйте через Redis Sets.


5. Write-through с инвалидацией

Обновляем кеш сразу при записи в БД.

def update_article(article_id, data):
    article = Article.objects.get(pk=article_id)
    for key, value in data.items():
        setattr(article, key, value)
    article.save()

    # Сразу обновляем кеш (не удаляем!)
    cache.set(f"article:{article_id}", article, timeout=3600)

    # Но списки всё равно инвалидируем
    cache.delete("homepage:articles")

Плюсы: Нет cache miss после обновления ❌ Минусы: Сложнее поддерживать консистентность списков


⚠️ Типичные ошибки

1. Забыли про связанные кеши
# ❌ Плохо — забыли про список
@receiver(post_save, sender=Article)
def invalidate(sender, instance, **kwargs):
    cache.delete(f"article:{instance.pk}")
    # А homepage:articles остался со старыми данными!
2. Race condition при обновлении
# ❌ Плохо — между get и set данные могут измениться
article = Article.objects.get(pk=1)  # версия 1
# ... кто-то обновил статью до версии 2 ...
cache.set("article:1", article)  # записали устаревшую версию 1!

Решение: Используйте get_or_set или версионирование.

3. Инвалидация в транзакции
# ❌ Плохо — кеш удалится, даже если транзакция откатится
with transaction.atomic():
    article.save()
    cache.delete(f"article:{article.pk}")  # Опасно!
    raise Exception("Rollback")  # Кеш уже удалён, но данные не сохранены

Решение: Инвалидируйте после коммита:

from django.db import transaction

def save_article(article):
    article.save()
    transaction.on_commit(
        lambda: cache.delete(f"article:{article.pk}")
    )

🎯 Рекомендации

Сценарий Рекомендуемый подход
Простой проект, допустима задержка TTL (5-15 минут)
Критична актуальность Event-based + сигналы
Много связанных сущностей Версионирование или теги
Высокая нагрузка на запись Write-through
Микросервисы Event-driven (через очереди)

💡 Паттерн: Централизованная инвалидация

Вместо разбросанных cache.delete() — один модуль:

# cache_utils.py
from django.core.cache import cache
from django.db import transaction


class CacheInvalidator:
    """Централизованная инвалидация кеша."""

    @staticmethod
    def invalidate_article(article_id: int, category_id: int = None):
        """Инвалидирует все кеши, связанные со статьёй."""
        keys = [
            f"article:{article_id}",
            "homepage:articles",
            "sitemap:articles",
        ]
        if category_id:
            keys.append(f"category:{category_id}:articles")

        cache.delete_many(keys)

    @staticmethod
    def invalidate_article_on_commit(article):
        """Безопасная инвалидация после коммита транзакции."""
        transaction.on_commit(
            lambda: CacheInvalidator.invalidate_article(
                article.pk,
                article.category_id
            )
        )

🧱 Кеширование view

from django.views.decorators.cache import cache_page


@cache_page(60 * 15)
def my_view(request):
    ...

Или в urls.py:

path("foo/", cache_page(900)(views.foo_view))

🧰 Кеширование шаблонов

{% load cache %}
{% cache 300 sidebar %}
    ...HTML sidebar...
{% endcache %}

🧩 Per-site кеш через middleware

MIDDLEWARE = [
    "django.middleware.cache.UpdateCacheMiddleware",
    # ...
    "django.middleware.cache.FetchFromCacheMiddleware",
]
CACHE_MIDDLEWARE_SECONDS = 300

🧼 Очистка кеша

cache.clear()

⛔ Отключение кеша для отдельной view

from django.views.decorators.cache import never_cache


@never_cache
def dynamic_view(request):
    ...

📌 Итоги

  • NoSQL — альтернатива реляционным БД для специфических сценариев (кеш, документы, графы).
  • CAP-теорема — в распределённой системе можно выбрать только 2 из 3: Consistency, Availability, Partition Tolerance.
  • Redis — универсальный инструмент для кеша, сессий, очередей, rate limiting.
  • Куки — данные на клиенте (браузере), используйте флаги безопасности.
  • Сессии — данные на сервере, привязанные к пользователю через sessionid.
  • Кеш — быстрая память для ускорения работы, не связана напрямую с пользователем.
  • Инвалидация кеша — критически важна, используйте сигналы или версионирование.

🧠 Вопросы для закрепления

  1. Чем отличаются куки от сессий?
  2. Почему сессии нельзя использовать для анонимных пользователей везде?
  3. Где по умолчанию Django хранит сессии?
  4. Что произойдёт, если модифицировать request.session['key']['value']?
  5. Как бы вы реализовали кеширование главной страницы?
  6. Как очистить все сессии, старше 30 дней?
  7. Какой SESSION_ENGINE лучше использовать в production и почему?
  8. Зачем нужны Signed Cookies?

📝 Практика на занятии

  1. Работа с сессиями:

    • Создайте view, которая считает количество посещений пользователя
    • Реализуйте "корзину" товаров через сессии
  2. Кеширование:

    • Закешируйте список статей на главной странице
    • Добавьте инвалидацию кеша при создании новой статьи

🏠 Домашнее задание

Задание 1: Счётчик просмотров статьи

Реализуйте счётчик просмотров для статей блога:

  • Используйте сессии, чтобы один пользователь не накручивал просмотры
  • Храните счётчик в Redis для быстродействия
  • Периодически синхронизируйте с БД

Задание 2: «Недавно просмотренные»

Реализуйте функционал «Недавно просмотренные статьи»:

  • Храните список последних 5 просмотренных статей в сессии
  • Отображайте их в сайдбаре

Задание 3: Rate Limiting

Реализуйте ограничение на количество комментариев:

  • Не более 5 комментариев в минуту от одного пользователя
  • Используйте Redis для хранения счётчиков
  • Показывайте пользователю, сколько секунд осталось до сброса лимита

Задание 4: Кеширование с инвалидацией

Для блога реализуйте:

  • Кеширование списка статей на 10 минут
  • Кеширование отдельной статьи на 1 час
  • Автоматическую инвалидацию при редактировании статьи
  • Используйте сигналы post_save и post_delete

⭐ Бонус: Сессии в Redis

  • Переключите хранение сессий на Redis (cached_db)
  • Сравните производительность с database backend
  • Напишите management-команду для миграции существующих сессий

← Лекция 24: ClassBaseView | Лекция 26: Логирование. Middleware. Signals. Messages. Manage commands →