显式调用cursor.close()的必要性

50

有时候我会使用connection.cursor()来执行原始查询,而不是使用ORM(因为ORM肯定不是万能解决方案)。

我注意到在几个地方,我没有在完成数据库查询后显式调用cursor.close()。到目前为止,这还没有导致任何错误或性能问题。我想知道如果不显式关闭游标会出现什么问题,可能会出现哪些问题?

据我所知,Django中的connectioncursor遵循“Python数据库API规范v2.0”(PEP-249)。根据规范,在调用__del__()方法时,cursor将自动关闭。我猜问题也可以这样表述:是否存在未调用该方法的情况?

补充信息,我使用的是Python 2.7和Django 1.6.5。


我猜这与这个问题非常相似 https://dev59.com/b2035IYBdhLWcg3wBLRL ?? - Aamir Rind
3
感谢 @AamirAdnan,不完全是——在发帖之前已经看过了。我知道如何关闭游标。问题是不关闭它有多糟糕,以及可能产生的后果。 - alecxe
5个回答

26
Django的cursor类只是对底层数据库的cursor的包装,因此保持cursor打开的效果基本上与底层数据库驱动程序相关。根据psycopg2(Django用于PostgreSQL数据库的DB驱动程序)FAQ,它们的游标是轻量级的,但会缓存使用游标对象进行查询返回的数据,这可能会浪费内存:
“游标是轻量级对象,创建很多游标不应该造成任何问题。但请注意,用于获取结果集的游标将缓存数据并按比例使用内存。我们建议几乎总是创建一个新的游标,并在不再需要数据时处理旧游标(在它们上调用close())。唯一的例外是紧密循环,在这种情况下通常使用相同的游标进行一堆INSERT或UPDATE。”
Django使用MySQLdb作为MySQL的后端,其中包括一些不同类型的游标,其中一些实际上将其结果集存储在服务器端。MySQLdb文档中对于Cursor.close指出,在完成使用服务器端游标时关闭它们非常重要:

如果您正在使用服务器端游标,则在完成使用并创建新游标之前关闭该游标非常重要。

然而,这对于Django并不相关,因为它使用由MySQLdb提供的默认Cursor类,该类在客户端上存储结果。保留已使用的游标只会浪费存储结果集所使用的内存,就像psycopg2一样。游标上的close方法仅会删除与数据库连接的内部引用并耗尽存储的结果集:
def close(self):
    """Close the cursor. No further queries will be possible."""
    if not self.connection: return
    while self.nextset(): pass
    self.connection = None

据我从查看源代码中了解到,Django 使用的所有剩余后端(cx_oraclesqlite3/pysqlite2)都遵循相同的模式;通过删除/重置存储的结果/对象引用来释放内存。sqlite3 文档甚至没有提到 Cursor 类有一个 close 方法,它仅在包含的示例代码中零星地使用。
当调用__del__()方法关闭cursor对象时,你说得对,cursor会被关闭,所以只有在保留cursor的长期引用时才需要显式地关闭;例如,如果你将self.cursor对象作为类的实例方法保留。

2
如果您将光标用于第二个查询,那么它为第一个查询缓存的数据将不再被引用。因此,在某种意义上,创建大量不必要的光标可能会适得其反,除非您明确删除已使用的光标。 - holdenweb
如果你要在短时间内进行大量查询,最好使用相同的游标。如果你要查询一次,然后在未来的某个未知时间再进行另一个查询,那么你应该关闭/删除游标,这样你就不会保留缓存结果集所使用的内存。 - dano
1
看起来我们有激烈的一致。 - holdenweb
Django(1.5+)最近对事务的原子处理方式进行了重大更改,特别是如果您经常使用原始查询,则值得注意。请查看最新的开发文档:https://docs.djangoproject.com/en/dev/topics/db/transactions/ - airstrike
contextlib.closing会确保游标被正确关闭,同时代码看起来还是很简洁。 - stanleyxu2005

13

__del__/.close():

  1. __del__不能保证被调用。
  2. 一些数据库在它们的__del__中不会调用cursor.close()(这是不好的实践,但确实存在)。
  3. 一些数据库实际上并不是在连接函数中创建连接,而是在游标函数中创建连接(例如对于2&3:pyhive的presto [也许他们已经修复了])。

关于服务器连接

大多数服务器都有一个空闲超时配置属性(我们称之为T)。如果一个连接空闲时间超过T秒,服务器将移除该连接。大多数服务器还有设置工作线程池大小(W)的属性。如果你已经有了W个连接到你的服务器,当尝试新建一个连接时,它可能会挂起。现在假设你没有显式关闭连接的选项,在这种情况下,你必须将超时设置得足够小,以便你的工作线程池从未完全使用,这取决于你有多少并发连接。

然而,如果你关闭游标/连接(即使不等价于[3],它们的行为方式相似),那么你就不必管理这些服务器配置属性,你的线程池只需要足够大,以管理所有并发连接(有时需要等待新资源)。我曾经见过一些服务器(例如Cassandra上的Titan)无法从线程池中耗尽的工作线程中恢复,因此整个服务器会崩溃直到重新启动。

简而言之

如果你使用像提到的那样非常成熟的库,你不会遇到问题。如果你使用不太完美的库,如果你不调用.close(),可能会在服务器获取工作线程时被阻塞,这取决于你的服务器配置和访问速率。


2

虽然操作系统通常可以释放资源,但关闭诸如数据库连接之类的内容以确保在不再需要时释放资源是良好的习惯,从数据库角度来看,真正重要的是确保任何更改都已经进行了commit()


2
明确调用cursor.close()可能有两个原因:
  1. __del__不能保证被调用,并且存在一些问题,您可以在这里这里阅读相关信息
  2. 显式比隐式更好(Python之禅

1

我有点晚回答这个问题。也许你需要一个在退出时关闭的作用域。

from contextlib import closing
from django.db import connection

with closing(connection.cursor()) as cursor:
    cursor.execute(...)
    cursor.execute(...)
    cursor.execute(...)

他们提到了使用Django 1.6版本,文档显示上下文管理器(例如你的示例)直到1.7版本才被明确添加。尽管如此,文档似乎表明在此之前也可以工作,因为“魔法方法查找中的意外行为”。 - Tim Tisdall
我同意上下文管理器绝对是最干净的方法。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接