如何强制 Django 忽略任何缓存并重新加载数据?

Posted

技术标签:

【中文标题】如何强制 Django 忽略任何缓存并重新加载数据?【英文标题】:How do I force Django to ignore any caches and reload data? 【发布时间】:2011-03-21 17:47:20 【问题描述】:

我正在使用来自未从 HTTP 请求调用的进程的 Django 数据库模型。该过程应该每隔几秒钟轮询一次新数据并对其进行一些处理。我有一个循环休眠几秒钟,然后从数据库中获取所有未处理的数据。

我看到的是,在第一次提取之后,该进程再也看不到任何新数据。我进行了一些测试,看起来 Django 正在缓存结果,即使我每次都在构建新的 QuerySet。为了验证这一点,我从 Python shell 中执行了此操作:

>>> 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() 方法的任何文档,也不知道它可能会做什么其他坏事。

我的问题是,为什么我会看到这种缓存行为,这与 Django docs 所说的相矛盾?以及如何防止它发生?

【问题讨论】:

【参考方案1】:

似乎count() 在第一次之后进入缓存。这是 QuerySet.count 的 django 源代码:

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 进行计数。 如果性能不是非常重要,我会照你的做,在count 之前调用update

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

【讨论】:

【参考方案2】:

您还可以在执行任何工作之前使用MyModel.objects._clone().count(). QuerySet 调用_clone() 中的所有方法 - 确保任何内部缓存都无效。

根本原因是MyModel.objects 每次都是同一个实例。通过克隆它,您将创建一个没有缓存值的新实例。当然,如果您希望使用相同的实例,您可以随时访问缓存并使缓存无效。

【讨论】:

这看起来是一个很棒且简单的解决方案,但至少在我的 Django 版本上,它不起作用。调用 MyModel.objects._clone() 会导致“AttributeError:'Manager' 对象没有属性 '_clone'”。我可以执行 MyModel.objects.all()._clone(),但这和以前一样 - 在我调用 update() 之前不会改变。我正在使用 Django 1.2.1。 我的错-应该是MyModel.objects.all()._clone()。考虑一下,您可以在没有_clone() 的情况下使用MyModel.objects.all().count()。这会创建一个新版本的基础对象,并且应该为您提供一个没有缓存值的新版本。也就是说,除非 Django 在那里做一些狡猾的事情,并用克隆体携带状态。 这个答案是错误的。在管理器上调用任何方法(如count())都会隐式克隆一个新的查询集,由于管理器身份,没有隐式缓存行为,并且不需要插入对_clone()all() 的无关调用。这整个思路是一条红鲱鱼,OP的真正问题是数据库级别的事务隔离,它与查询集或Django级别的缓存根本无关。 自从我玩弄这个特定问题以来已经有很长时间了,但我想在创建 count() 时有某种缓存,否则,卡尔是正确的,这个答案很遥远。【参考方案3】:

我们在强制 django 刷新“缓存”方面付出了相当大的努力——事实证明,这根本不是缓存,而是由于事务的产物。这可能不适用于您的示例,但肯定在 django 视图中,默认情况下,有一个对事务的隐式调用,然后 mysql 将其与您启动时其他进程发生的任何更改隔离开来。

我们使用了@transaction.commit_manually 装饰器,并在您需要最新信息的每个场合之前调用transaction.commit()

正如我所说,这绝对适用于视图,不确定它是否适用于不在视图内运行的 django 代码。

这里有详细信息:

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

【讨论】:

这个答案大部分是比较准确的,但是第一句话是高度误导的。这个问题与Django中的任何“缓存”或“强制django刷新”无关,完全是数据库级别的事务隔离。【参考方案4】:

遇到这个问题并找到了两个明确的解决方案,我认为值得发布另一个答案。

这是 MySQL 默认事务模式的问题。 Django 在开始时会打开一个事务,这意味着默认情况下您不会看到数据库中所做的更改。

这样展示

在终端 1 中运行 django shell

>>> 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 for MySQL以更改默认事务模式

transaction-isolation = READ-COMMITTED

请注意,这是 Mysql 的一个相对较新的功能,并且有 some consequences for binary logging / slaving。如果你愿意,你也可以把它放在 django 连接序言中。

3 年后更新

现在 Django 1.6 有 turned on autocommit in MySQL 这不再是问题。上面的示例现在可以在没有 flush_transaction() 代码的情况下正常工作,无论您的 MySQL 处于 REPEATABLE-READ(默认)还是 READ-COMMITTED 事务隔离模式。

在非自动提交模式下运行的以前版本的 Django 中发生的情况是第一个 select 语句打开了一个事务。由于 MySQL 的默认模式是 REPEATABLE-READ,这意味着后续的 select 语句将不会读取数据库更新 - 因此需要上面的 flush_transaction() 代码来停止事务并启动新事务。

尽管如此,您可能仍需要使用READ-COMMITTED 事务隔离的原因。如果您要将终端 1 放入事务中,并且希望查看终端 2 的写入,则需要 READ-COMMITTED

flush_transaction() 代码现在会在 Django 1.6 中产生弃用警告,因此我建议您将其删除。

【讨论】:

DATABASE_OPTIONS = "init_command": "SET storage_engine=INNODB, SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED", 从 django 1.2 开始,设置的语法已经改变。将“OPTIONS”添加到您的 DATABASES 设置(可能是“默认”设置)“OPTIONS”:“init_command”:“SET storage_engine=INNODB,SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED”, 嗯,我在 Django 1.8 上,问题仍然存在,无论我使用 MySQL 还是 SQLite 我在 Django 1.10 上试过这个,但它不适用于 sqlite 或 pg。【参考方案5】:

我不确定我是否会推荐它......但你可以自己杀死缓存:

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

这是一种更好的技术,它不依赖于摆弄 QuerySet 的内部结构:请记住,缓存发生在 QuerySet 中,但刷新数据只需要底层 查询 重新执行。 QuerySet 实际上只是一个包装 Query 对象的高级 API,外加一个用于 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】:

如果您将.all() 附加到查询集,它将强制从数据库中重新读取。尝试 MyModel.objects.all().count() 而不是 MyModel.objects.count()

【讨论】:

这个其实更干净 all() 创建 QuerySet 的新对象,_result_cache 为空。

以上是关于如何强制 Django 忽略任何缓存并重新加载数据?的主要内容,如果未能解决你的问题,请参考以下文章

Django缓存:缓存预热时重新加载浏览器缓存

如何强制重新加载缓存的 HTML 文件

如何强制浏览器重新加载缓存的 CSS 和 JavaScript 文件

Chrome浏览器如何强制刷新

强制回调以重新加载文件

如何使表单数据在“从缓存重新加载”时持续存在