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


77

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

我看到的是,在第一次获取之后,该进程再也看不到任何新数据。我进行了一些测试,尽管每次我都在构建新的QuerySet,但看起来Django正在缓存结果。为了验证这一点,我从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文档所说的相反?以及如何防止它发生?

Answers:


95

遇到了这个问题并找到了两个确定的解决方案后,我认为值得再次提出答案。

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

替代解决方案是更改MySQL的my.cnf以更改默认事务模式

transaction-isolation = READ-COMMITTED

请注意,这对于Mysql是一个相对较新的功能,会对二进制日志记录/从属产生一些影响。如果需要,您也可以将其放在django连接序言中。

3年后更新

现在,Django 1.6已在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中会产生弃用警告,因此建议您将其删除。


7
DATABASE_OPTIONS = {“ init_command”:“ SET storage_engine = INNODB,会话事务隔离级别已
提交

从Django 1.2开始,设置的语法已更改。将“ OPTIONS”添加到您的数据库设置中(可能是“默认”一项)'OPTIONS':{“ init_command”:“ SET storage_engine = INNODB,会话事务隔离级别已
提交

2
好吧,我使用的是Django 1.8,无论我使用MySQL还是SQLite,问题仍然存在
tgdn 2015年

我在Django 1.10上尝试过此功能,但不适用于sqlite或pg。
杰瑞德(Jared)

8

我们在强迫django刷新“缓存”方面付出了很多努力-事实证明这并不是真正的缓存,而是由于事务导致的工件。这可能不适用于您的示例,但是肯定在django视图中,默认情况下,存在对事务的隐式调用,然后mysql将它与启动时其他进程发生的任何更改隔离开来。

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

就像我说的,这肯定适用于视图,不确定它是否适用于不在视图内运行的Django代码。

详细信息在这里:

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


这个答案大部分是相对准确的,但第一句话极易引起误解。这个问题与Django中的任何“缓存”或“强制django刷新”无关,它完全与数据库级事务隔离有关。
卡尔·梅耶

7

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

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

这是一种更好的技术,它不依赖于QuerySet的内部:请记住,缓存是在QuerySet内进行的,但是刷新数据仅需要重新执行基础Query。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()

相当容易!您当然可以将其实现为辅助功能,并根据需要使用。


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


-1

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

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


这看起来像一个很棒且简单的解决方案,但是至少在我的Django版本上,它不起作用。调用MyModel.objects._clone()会导致“ AttributeError:'Manager'对象没有属性'_clone'”。我可以做MyModel.objects.all()._ clone(),但是它像以前一样工作-在我调用update()之前不会改变。我正在使用Django 1.2.1。
scippy

我的坏-应该是MyModel.objects.all()._clone()。考虑这一点时,您可以MyModel.objects.all().count()不用做一个_clone()。这将创建基础对象的新版本,并且应该为您提供没有缓存值的新版本。那就是说,除非Django在那儿做一些曲折的事情,并带有克隆的状态。
特拉维斯·斯威斯古德

3
这个答案是错误的。count()在管理器上调用任何方法(如)都将隐式克隆一个新的查询集,由于管理器的身份,没有隐式的缓存行为,也无需插入对_clone()或的多余调用all()。整个思路是一团糟,OP的真正问题是数据库级事务隔离,它与查询集或Django级缓存完全无关。
卡尔·梅耶

自从我考虑了这个特定问题以来已经有很长时间了,但是我想这一点在创建时count()就已经有了某种缓存,否则,Carl是正确的,这个答案还遥遥无期。
Travis Swicegood,2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.