Код-ревью 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 блокировка на пересчёт: Надёжнее отдавать устаревшее значение, пока один процесс обновляет кеш в фоне.
Что должен найти ревьюер
- Безопасностьстрока 8Ключ без пользователя, а данные персональные (
build_dashboard(request.user)): все увидят дашборд первого пользователя - Ошибкастрока 9
if not dataпутает пустое значение с промахом; проверятьis None - Ошибкастрока 11Нет TTL: значение живёт вечно; плюс pickle в кеше ломается при смене классов и опасен, если кеш скомпрометирован
- Ошибкастрока 17Инвалидация до коммита: параллельный запрос успеет закешировать старую цену; удалять в
transaction.on_commit - Производительностьстрока 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 ошибок.