长时间运行的 Web 服务器应用程序常常会将后端 RDBMS 用于很多不同的目的,这一点在事务性应用程序中尤为突出。实际上,我认为,对比实际的使用情况,应该 有更多的 Web 服务去使用数据库 ,因为开发人员在配置 RDBMS 时经常会感觉到一种不必要的负担(不过那是另一篇文章的主题)。在其众多的优势中,数据库访问至少存在一个瓶颈:连接。
连接产生瓶颈的方式有两个。简单地说,它们需要占用一些带宽以便在网络上传输,并会占用一点 CPU 和 内存资源以便在 RDBMS 机上分配每一个连接。占用资源虽不多,但却不能忽视。然而,更重要的是 RDBMS 只提供有限 数量的连接。如果 RDBMS 的主客户机是一个 Web 应用程序,那么这个数量限制是对每个客户机而言还是对总体而言将不会有什么实际上的差别,原因是数据库客户机(Web 服务器)会创建大批动作。
在本文介绍的这个技巧里,我使用的是优秀的 psycopg2 适配器,但任何使用了 DBAPI 的 Python 数据库也会得到同样的效果。这里需要特别注意的是对象关系映射(ORM),在我看来,它隐藏了太多数据库访问真实情况的细节,强加了一些人为限制
,而且 “插手” 的事情太多,远远超出了简化编程的初衷。
在理想的情况下(没有连接池),每个线程或进程(Web 服务器应用程序在其中连接到数据库)都会很快获得连接并创建游标,然后执行一些相对快速的操作、提交或回滚这些操作,然后关闭此连接。如果一切顺利,这会非常简单。例如:
清单 1. Web 服务器写入 RDBMS
def AddData(foo, bar):
"This function is generally called in its own thread"
from psycopg import connect
conn = connect("dbname=transact user=web host=server.mine")
cur = conn.cursor()
cur.execute("INSERT INTO userdata VALUES (%r, %r)" % (foo, bar))
conn.commit()
conn.close()
|
上述示例代码还不是最佳的代码,因为我完全可以围绕 connect() 调用再放上一个 try/'except',也可以再用一个 try/finally 来确保 conn.close() 被调用。我本来也可以使用 DBAPI 值转义。尽管如此,AddData() 基本上已经具备了典型的数据库调用的 “形态”。
与上述类似的代码中很可能会出现问题。一个问题就是当很多线程被同时启用时,即使还没有达到该数据库的限值,其中的一些线程实际上也不能获得连接。从理论上讲,这个问题不应该 发生,但实际它却发生了。另一个问题是,如果达到 了该数据库池限值(通常是因为这些线程的完成用了比预期更长的时间或是这些线程在关闭连接前引起了异常),获取连接还是有可能失败。
通过创建一个本地线程池,这两个问题都能得到解决。连接池是一个预分配数据库连接的集合,这些连接可以由每个 “虚拟连接” 线程重用或释放。也就是说,真正 的数据库连接会相对较少关闭,但它们的池代理则会随着每个事务获取或释放。
一方面,从一个本地池中获取预分配连接是个内存内操作,除非连接已耗尽,否则这种操作不太可能失败,因为不会有网络或数据库延时问题出现。另一方面,使用池可以让您精确地本地管理连接限值。如果依靠数据库来限制连接,将不能直接判断在应用程序层面有多少连接已用和还有多少连接可用;已打开线程的数量虽可以作为粗略的近似数字,但却不是一个可靠的方法。在这种情况下,antipool.py 模块会在后台增减池化了的(实际的)连接以免将实际连接保存过长。然而,对用户来说,除了池中连接的可用性是本地已知的,且其最大值是连接池的参数而非数据库的参数这两点外,池化了的连接看上去与实际的数据库连接没什么区别。
即使使用 antipool.py,我仍然发现进一步对池进行些许包装会很方便,而不必时时都使用 ConnectionPool 的所有选项。此外,对于大多数达到了实际连接限值的常见情况而言,一个简单的 sleep() 通常足以最终获取一个新的连接。如果大量线程都运行不正常(一直不能关闭),虽然仍存在问题,但至少调试起来会更加容易。如下所示的是一个我使用的包装器:
清单 2. webapp_pool.py
"""USAGE:
from webapp_pool import get_connection
conn, cur = get_connection() # Might hang, but never raises
cur.execute(SQL)
conn.commit()
conn.release() # 'conn.release()' not 'conn.close()'
"""
import psycopg2
from time import sleep
from antipool import ConnectionPool
from database import host, database, user, MAXCONNECTIONS
conn_pool = ConnectionPool(psycopg2,
host=host,
database=database,
user=user,
options={'maxconn':MAXCONNECTIONS})
def get_connection():
got_connection = False
while not got_connection:
try:
conn = conn_pool.connection()
cur = conn.cursor()
got_connection = True
except psycopg2.OperationalError, mess:
# Might log exception here
sleep(1)
except AttributeError, mess:
# Might log exception here
sleep(1)
return conn, cur
|
在本文所介绍的这个技巧里,我使用了数据库未知的 antipool.py 模块。我发现 antipool.py 非常灵活;在这篇简短的文章中,它的许多其他优势特性未能一一论及。但如果您打算使用内置到 psycopg2 或其他工具的池化功能,那么使用连接池化的这些常见原则仍适用。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- 查看 Martin Blais 的全部 antiorm 包。
- IBM 的 DB2 RDBMS 也有一个 Python 适配器,称为 PyDB2(如您所想)。该模块不包含连接池化,但可与
antipool.py完美协作。 - 若想使用 IBM 先进的 DB2 RDBMS,一定要访问可免费下载的 beta of DB2/Viper。
- 要获得
psycopg2,可借助其 Trac 主页。请务必使用 2.x 系列,而非备受争议的 1.x。 - 参看 developerWorks
特色试用软件下载。
-
订阅 developerWorks Web development 新闻。
-
从 Web
development 专区技术库 获取更多指导文章。
讨论
-
参与 developerWorks
社区:blogs、论坛 等等。

David Mertz 是一个并发的狂热者,但常为连接伤脑筋。您可以通过 mertz@gnosis.cx 和 David 联系,他的生活点滴记录在 http://g nosis.cx/publish/。他的书可以在 http://gnosis.cx/TPiP/ 买到。