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

时间:2010-07-27 17:17:44

标签: python django caching

我正在使用来自未通过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

如您所见,添加新数据不会更改结果计数。但是,调用manager的update()方法似乎可以解决问题。

我找不到有关该update()方法的任何文档,也不知道它可能会做些什么坏事。

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

6 个答案:

答案 0 :(得分:93)

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

这是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'
>>> 

现在读取新数据

以下是带有docstring

的易粘贴块中的代码
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中生成弃用警告,因此我建议您将其删除。

答案 1 :(得分:8)

我们在强迫django刷新“缓存”方面遇到了一些麻烦 - 事实证明,由于交易原因,它实际上并不是一个缓存。这可能不适用于您的示例,但肯定在django视图中,默认情况下,会对事务进行隐式调用,然后mysql会从您启动的其他进程中发生的任何更改中隔离出来。

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

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

详细信息:

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

答案 2 :(得分:6)

似乎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 如果表现不是非常重要,我会做你正在做的事情,在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 :(得分:6)

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

>>> 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,还有一个用于查询结果的容器(带缓存!)。因此,给定一个查询集,这里是一种强制刷新的通用方法:

>>> 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()

非常简单!您当然可以将其实现为辅助函数并根据需要使用。

答案 4 :(得分:2)

如果将.all()附加到查询集,它将强制重新读取数据库。尝试 MyModel.objects.all().count()代替MyModel.objects.count()

答案 5 :(得分:-1)

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

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