如何强制Django忽略缓存并重新加载数据?
我在使用Django的数据库模型,但这个过程不是通过HTTP请求来调用的。这个过程是每隔几秒钟就去检查一下有没有新数据,然后对这些数据进行处理。我有一个循环,每次暂停几秒钟,然后从数据库中获取所有未处理的数据。
我发现第一次获取数据后,之后的过程再也看不到新数据了。我做了一些测试,发现即使我每次都在创建新的查询集,Django似乎还是在缓存结果。为了确认这一点,我在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()方法的任何文档,也不知道它可能会带来什么其他问题。
我的问题是,为什么我会看到这种缓存行为,这和Django文档上说的相矛盾?我该如何防止这种情况发生呢?
6 个回答
我不太确定我是否推荐这样做……不过你可以自己清除缓存:
>>> 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 对象,还有一个用来存放(并缓存)查询结果的容器。因此,给定一个 queryset,这里有一种通用的方法可以强制刷新:
>>> 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()
这很简单!当然,你可以把这个实现成一个辅助函数,按需使用。
我们在让Django刷新“缓存”方面遇到了一些困难,结果发现这根本不是缓存,而是由于事务造成的一个现象。这可能不适用于你的例子,但在Django的视图中,默认情况下,会隐式调用一个事务,这样MySQL就会把你开始时的任何变化隔离开来,不会受到其他进程的影响。
我们使用了@transaction.commit_manually
这个装饰器,并在每次需要最新信息之前调用transaction.commit()
。
正如我所说,这绝对适用于视图,不确定是否适用于不在视图中运行的Django代码。
详细信息请见这里:
我遇到过这个问题,并找到了两个明确的解决方案,所以我觉得有必要再分享一个答案。
这个问题出在MySQL的默认事务模式上。Django在开始时会打开一个事务,这意味着默认情况下你在数据库中看到的更改是不可见的。
我们可以这样演示这个问题:
在终端1中运行一个django shell
>>> MyModel.objects.get(id=1).my_field
u'old'
然后在终端2中再运行一个django shell
>>> 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()
另一种解决方案是修改MySQL的my.cnf文件,以改变默认的事务模式。
transaction-isolation = READ-COMMITTED
需要注意的是,这对MySQL来说是一个相对较新的功能,并且对二进制日志/从属有一些影响。你也可以把这个放在django连接的前言中,如果你愿意的话。
三年后的更新
现在Django 1.6已经在MySQL中开启了自动提交,所以这个问题不再存在。上面的例子现在可以正常工作,无论你的MySQL是在REPEATABLE-READ
(默认)还是READ-COMMITTED
的事务隔离模式下,都不需要flush_transaction()
这段代码。
在之前的Django版本中,由于没有开启自动提交,第一次select
语句会打开一个事务。因为MySQL的默认模式是REPEATABLE-READ
,这意味着后续的select
语句无法读取到数据库的更新,因此需要上面的flush_transaction()
代码来结束当前事务并开始一个新的事务。
不过,仍然有理由让你想使用READ-COMMITTED
的事务隔离。如果你在终端1中开启了一个事务,而你想看到终端2中的写入内容,你就需要READ-COMMITTED
。
在Django 1.6中,flush_transaction()
这段代码现在会产生一个弃用警告,所以我建议你把它去掉。