Код-ревью Middle Кеширование обновлено

Код-ревью кеширования профиля и рейтинга

Вопрос

Найдите проблемы в функциях, которые кешируют данные в Redis.

import pickle
import redis

r = redis.Redis()


def get_dashboard(request):
    data = r.get('dashboard')
    if not data:
        data = build_dashboard(request.user)
        r.set('dashboard', pickle.dumps(data))
        return data
    return pickle.loads(data)


def update_price(product_id, price):
    r.delete(f'product:{product_id}')
    with transaction.atomic():
        Product.objects.filter(pk=product_id).update(price=price)


def top_products():
    key = 'top'
    cached = r.get(key)
    if cached is None:
        cached = expensive_top_query()
        r.set(key, json.dumps(cached), ex=60)
        return cached
    return json.loads(cached)

Коротко

Исправления: ключ dashboard:{userid} с TTL и сериализацией в JSON; проверка if data is None; удаление ключа товара через transaction.oncommit(lambda: r.delete(key)); для topproducts блокировка на пересчёт: Надёжнее отдавать устаревшее значение, пока один процесс обновляет кеш в фоне.

Что должен найти ревьюер

  1. Безопасностьстрока 8
    Ключ без пользователя, а данные персональные (build_dashboard(request.user)): все увидят дашборд первого пользователя
  2. Ошибкастрока 9
    if not data путает пустое значение с промахом; проверять is None
  3. Ошибкастрока 11
    Нет TTL: значение живёт вечно; плюс pickle в кеше ломается при смене классов и опасен, если кеш скомпрометирован
  4. Ошибкастрока 17
    Инвалидация до коммита: параллельный запрос успеет закешировать старую цену; удалять в transaction.on_commit
  5. Производительностьстрока 26
    Горячий ключ без защиты от stampede: при истечении все запросы одновременно выполнят тяжёлый запрос

Разбор

Исправления: ключ dashboard:{user_id} с TTL и сериализацией в JSON; проверка if data is None; удаление ключа товара через transaction.on_commit(lambda: r.delete(key)); для top_products блокировка на пересчёт:

def top_products():
    cached = r.get('top')
    if cached is not None:
        return json.loads(cached)
    if r.set('lock:top', '1', nx=True, ex=30):
        try:
            value = expensive_top_query()
            r.set('top', json.dumps(value), ex=60 + random.randint(0, 10))
            return value
        finally:
            r.delete('lock:top')
    time.sleep(0.05)
    return top_products_fallback()

Надёжнее отдавать устаревшее значение, пока один процесс обновляет кеш в фоне. Снимать блокировку стоит со сверкой своего токена, чтобы не удалить чужую.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.