我刚做Python开发那会,最烦的就是数据库连接泄漏。代码跑几分钟没问题,放生产环境跑一天,数据库连接池就炸了。我翻日志,全是“Too many connections”。后来我找到了一个写法,再也没出过这个问题。
核心原理就是Python的上下文管理器,也就是 with 语句。这个机制天生就是为了资源管理设计的。你打开一个文件,用with,文件会自动关闭。操作数据库也一样道理。
很多人写数据库连接代码是这样的:
conn = pymysql.connect(...) cursor = conn.cursor()
cursor.execute(sql)
data = cursor.fetchall()
conn.close()
我见过太多人只写了conn.close(),忘了cursor.close()。更常见的坑是代码中间抛了异常,后面关闭连接的代码根本没执行。连接就这么丢了。
用上下文管理器改写后,模样是这样:
from contextlib import contextmanager @contextmanager
def get_db():
conn = pymysql.connect(host='localhost', user='root', password='123456', database='test')
try:
yield conn
finally:
conn.close()
使用的时候就简单了:
with get_db() as conn: with conn.cursor() as cursor:
cursor.execute('SELECT FROM users')
result = cursor.fetchall()
这里 yield 前面的代码是准备阶段,连接数据库。yield后面的代码是清理阶段,关闭连接。就算yield那一块代码报错了,finally里的conn.close()也一定会执行。这点我测试过无数次。
有人说这样写太啰嗦。你想想,你手动写关闭代码,每个函数都要写try...finally,还得记着把游标也关了。有一次我在循环里操作数据库,忘了关连接,循环几千次后数据库直接拒绝连接了。用contextmanager,一套写法到处用。
我还见过一种写法,直接在类里实现了__enter__和__exit__方法。那个也行,但不如这个装饰器版本简洁。我团队里新来的小伙子,看我这么写,五分钟就学会了。他原来写数据库代码每次都自己try...finally,经常漏掉游标的关闭。
分享一个我踩过的坑。有人把连接对象的创建放在了with块外面,只在with里yield了cursor。这样做有个问题,连接对象在with块结束后可能没被销毁。正确的做法是把连接对象和游标的创建都放在上下文管理器内部,外面只管用。我后来改成了嵌套写法,游标也在上下文管理器里处理:
@contextmanager def get_cursor():
conn = pymysql.connect(...)
cursor = conn.cursor()
try:
yield cursor
finally:
cursor.close()
conn.close()
这个写法更保险。游标和连接一并关闭。生产环境跑了一个月,连接数一直稳定在个位数。之前手动管理的版本,连接数经常冲到一百多。
你写代码的时候,只要看到要手动close的资源,就直接套上上下文管理器。数据库连接、文件句柄、网络连接、甚至锁对象,都可以这么处理。我养成习惯后,再也没为资源泄漏发过愁。
最后说个细节,contextmanager这个装饰器来自contextlib模块,是标准库自带的。不用装任何第三方包。你导入就行。我遇到很多人不知道有这个模块,自己手写类实现上下文管理器协议,又累又容易错。
干开发这行的,谁没被数据库连接泄漏折磨过。这个写法不解决所有问题,但至少让你在打开资源的时候,不再担心忘记关闭。你每写一个数据库操作,都套上这个模式。连接泄漏,从此跟你没关系。