Как заставить Django игнорировать кеши и перезагружать данные? - PullRequest
73 голосов
/ 27 июля 2010

Я использую модели базы данных Django из процесса, который не вызывается из HTTP-запроса.Предполагается, что процесс будет запрашивать новые данные каждые несколько секунд и обрабатывать их.У меня есть цикл, который спит в течение нескольких секунд, а затем получает все необработанные данные из базы данных.

Я вижу, что после первой выборки процесс никогда не видит новых данных.Я провел несколько тестов и похоже, что Django кеширует результаты, хотя я каждый раз создаю новые QuerySets.Чтобы убедиться в этом, я сделал это из оболочки Python:

>>> MyModel.objects.count()
885
# (Here I added some more data from another process.)
>>> MyModel.objects.count()
885
>>> MyModel.objects.update()
0
>>> MyModel.objects.count()
1025

Как видите, добавление новых данных не меняет счетчик результатов.Тем не менее, вызов метода update () менеджера, похоже, решает проблему.

Я не могу найти никакой документации по этому методу update () и понятия не имею, какие другие плохие вещи он может сделать.1009 * Мой вопрос: почему я вижу такое поведение кэширования, которое противоречит тому, что говорят Django ?И как мне предотвратить это?

Ответы [ 6 ]

93 голосов
/ 17 октября 2011

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

Это проблема в режиме транзакций по умолчанию в MySQL.Django открывает транзакцию при запуске, что означает, что по умолчанию вы не увидите изменений, внесенных в базу данных.

Продемонстрируйте, как это

Запустите оболочку django в терминале 1

>>> MyModel.objects.get(id=1).my_field
u'old'

И еще один в терминале 2

>>> MyModel.objects.get(id=1).my_field
u'old'
>>> a = MyModel.objects.get(id=1)
>>> a.my_field = "NEW"
>>> a.save()
>>> MyModel.objects.get(id=1).my_field
u'NEW'
>>> 

Вернемся к терминалу 1, чтобы продемонстрировать проблему - мы все еще читаем старое значение из базы данных.

>>> MyModel.objects.get(id=1).my_field
u'old'

Сейчас в терминале1 демонстрирует решение

>>> from django.db import transaction
>>> 
>>> @transaction.commit_manually
... def flush_transaction():
...     transaction.commit()
... 
>>> MyModel.objects.get(id=1).my_field
u'old'
>>> flush_transaction()
>>> MyModel.objects.get(id=1).my_field
u'NEW'
>>> 

Новые данные теперь считываются

Вот этот код в легко вставляемом блоке с строкой документации

from django.db import transaction

@transaction.commit_manually
def flush_transaction():
    """
    Flush the current transaction so we don't read stale data

    Use in long running processes to make sure fresh data is read from
    the database.  This is a problem with MySQL and the default
    transaction mode.  You can fix it by setting
    "transaction-isolation = READ-COMMITTED" in my.cnf or by calling
    this function at the appropriate moment
    """
    transaction.commit()

Альтернативное решение:изменить my.cnf для MySQL, чтобы изменить режим транзакции по умолчанию

transaction-isolation = READ-COMMITTED

Обратите внимание, что это относительно новая функция для Mysql и имеет некоторые последствия для двоичной регистрации / подчинения .Вы также можете добавить это в преамбулу подключения django, если хотите.

Обновление через 3 года

Теперь, когда в Django 1.6 включена , включена автокоммит в MySQL это больше не проблема.Приведенный выше пример теперь отлично работает без кода flush_transaction() независимо от того, находится ли ваш MySQL в режиме изоляции транзакций REPEATABLE-READ (по умолчанию) или READ-COMMITTED.

Что происходило в предыдущих версиях Django, которые работали вРежим autocommit состоял в том, что первый оператор select открыл транзакцию.Поскольку режим MySQL по умолчанию - REPEATABLE-READ, это означает, что последующие операторы select не будут считывать обновления базы данных - следовательно, необходим код flush_transaction(), описанный выше, который останавливает транзакцию и запускает новую.

Есть еще причины, по которым вы можете использовать READ-COMMITTED изоляцию транзакций.Если вы хотите включить в транзакцию терминал 1 и хотите видеть записи из терминала 2, вам потребуется READ-COMMITTED.

Код flush_transaction() теперь выдает предупреждение об устаревании в Django 1.6, поэтому я рекомендуювы удалите его.

8 голосов
/ 23 сентября 2010

Мы изо всех сил пытались заставить django обновить «кеш», который, как оказалось, на самом деле был вовсе не кеш, а артефактом из-за транзакций.Это может не относиться к вашему примеру, но, безусловно, в представлениях django по умолчанию есть неявный вызов транзакции, который затем mysql изолирует от любых изменений, которые происходят в других процессах после запуска.

мы использовали@transaction.commit_manually декоратор и вызовы transaction.commit() непосредственно перед каждым случаем, когда вам нужна актуальная информация.

Как я уже сказал, это определенно относится к представлениям, не уверен, применимо ли это к коду django, невыполняется внутри представления.

подробная информация здесь:

http://devblog.resolversystems.com/?p=439

6 голосов
/ 03 февраля 2015

Я не уверен, что порекомендовал бы это ... но вы можете просто убить кеш самостоятельно:

>>> qs = MyModel.objects.all()
>>> qs.count()
1
>>> MyModel().save()
>>> qs.count()  # cached!
1
>>> qs._result_cache = None
>>> qs.count()
2

А вот лучшая техника, которая не зависит от игры с внутренностямиQuerySet: Помните, что кэширование происходит в QuerySet , но для обновления данных просто необходимо выполнить базовый Query .QuerySet - это на самом деле просто высокоуровневый API, обертывающий объект Query, плюс контейнер (с кэшированием!) Для результатов Query.Таким образом, учитывая набор запросов, вот универсальный способ принудительного обновления:

>>> MyModel().save()
>>> qs = MyModel.objects.all()
>>> qs.count()
1
>>> MyModel().save()
>>> qs.count()  # cached!
1
>>> from django.db.models import QuerySet
>>> qs = QuerySet(model=MyModel, query=qs.query)
>>> qs.count()  # refreshed!
2
>>> party_time()

Довольно просто!Конечно, вы можете реализовать это как вспомогательную функцию и использовать по мере необходимости.

6 голосов
/ 27 июля 2010

Похоже, что count() отправляется в кэш после первого раза.Это исходный код django для QuerySet.count:

def count(self):
    """
    Performs a SELECT COUNT() and returns the number of records as an
    integer.

    If the QuerySet is already fully cached this simply returns the length
    of the cached results set to avoid multiple SELECT COUNT(*) calls.
    """
    if self._result_cache is not None and not self._iter:
        return len(self._result_cache)

    return self.query.get_count(using=self.db)

update, кажется, выполняет довольно много дополнительной работы, помимо того, что вам нужно.
Но я не могу думать ни о какомлучший способ сделать это, если не писать собственный SQL для подсчета.
Если производительность не так важна, я бы просто сделал то, что вы делаете, вызывая update до count.

QuerySet.update:

def update(self, **kwargs):
    """
    Updates all elements in the current QuerySet, setting all the given
    fields to the appropriate values.
    """
    assert self.query.can_filter(), \
            "Cannot update a query once a slice has been taken."
    self._for_write = True
    query = self.query.clone(sql.UpdateQuery)
    query.add_update_values(kwargs)
    if not transaction.is_managed(using=self.db):
        transaction.enter_transaction_management(using=self.db)
        forced_managed = True
    else:
        forced_managed = False
    try:
        rows = query.get_compiler(self.db).execute_sql(None)
        if forced_managed:
            transaction.commit(using=self.db)
        else:
            transaction.commit_unless_managed(using=self.db)
    finally:
        if forced_managed:
            transaction.leave_transaction_management(using=self.db)
    self._result_cache = None
    return rows
update.alters_data = True
3 голосов
/ 28 июня 2016

Если вы добавите .all() в набор запросов, это приведет к повторному считыванию из БД.Попробуйте MyModel.objects.all().count() вместо MyModel.objects.count().

0 голосов
/ 28 июля 2010

Вы также можете использовать MyModel.objects._clone().count(). Все методы в вызове QuerySet _clone() перед выполнением любой работы - это гарантирует, что любые внутренние кэши будут недействительными.

Основная причина в том, что MyModel.objects является одним и тем же экземпляром каждый раз. Клонируя его, вы создаете новый экземпляр без кэшированного значения. Конечно, вы всегда можете получить доступ к кешу и сделать его недействительным, если вы предпочитаете использовать один и тот же экземпляр.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...