使用 Python和Oracle 数据库实现高并发性

本文将简单介绍如何使用 Python 和 Oracle 数据库构建并发应用程序,描述如何使用 Python 代码利用线程与 Oracle 数据库交互,并解释如何将 SQL 查询并行提交到数据库服务器而不是依次处理。您还将了解如何让 Oracle 数据库处理并发性问题以及如何利用 Python 事件驱动的框架 Twisted。

Python 中的多线程编程

线程是并行处理中的一个非常有用的特性。如果您的一个程序正在执行耗时的操作并且可以将其分成若干个独立的任务并行执行,那么使用线程可以帮助您构建更加高效、快速的代码。多线程的另一个有趣的用处是可以提高应用程序的响应能力 — 在后台执行耗时操作的同时,主程序仍然可以做出响应。

当长时间运行的 SQL 语句彼此并无关联并且可以并行执行时,将这些语句封装到 Python 的不同线程中是不错的做法。例如,如果 Web 页面将初始的 SQL 查询并行提交到数据库服务器而不是按顺序处理它们(使它们一个接一个地排队等待),则可显著减少 Web 页面的加载时间。

当您需要将某些大对象 (LOB) 上载到数据库时,也会发现线程很有用。以并行方式执行此操作不仅可以减少将 LOB 上载到数据库所需的整体时间,还可以在后台进行并行上载的同时保持程序主线程的响应能力。

假设您需要将几个二进制大对象 (BLOB) 上载到数据库并将其保存到 blob_tab 表(您可能已经在自定义数据库模式中创建了该表),如下所示:

首先,我们来了解一下如何不利用线程将 BLOB 一个接一个地存储到 blob_tab 表中。以下 Python 脚本可以完成该任务,永久保存分别使用文件名和 URL 获得的两个输入图像。该示例假设您已经在 usr/pswd 自定义数据库模式中创建了 blob_tab 表和 blob_seq 序列:

尽管获取和存储 figure1.bmp 和 figure2.bmp 的任务在此处一个接一个地进行,但是,您可能已经猜到,这些任务实际上并不存在顺序上的先后关联性。因此,您可以重构上述代码,使其在单个线程中读取和存储每个图像,从而通过并行处理提升性能。在这种特殊的情况下值得一提的是,您不必协调并行运行的线程,从而可以极大地简化编码。

以下示例显示了如何利用面向对象的方法重新编写上述脚本以使用线程。具体来说,该示例说明了如何从 threading 模块扩展 Thread 类,针对特定任务对其进行自定义。

在本示例中,您将在两个线程间共享一个连接,但是将为每个线程创建一个单独的游标对象。此处,读取 BLOB 然后将其插入数据库的操作是在 threading.Thread 标准 Python 类中 AsyncBlobInsert 自定义子类的改写的 run 方法中实现的。因此,要在单独的线程中开始上载 BLOB,您只需创建一个 AsyncBlobInsert 实例,然后调用其 start 方法。在上述代码中,注意 threaded 属性的使用,该属性作为参数传递到 cx_Oracle.connect 方法。通过将其设置为 true,您指示 Oracle 数据库使用 OCI_THREADED 模式(又称为 threaded 模式),从而指明应用程序正在多线程环境中运行。请注意,在此处针对单线程应用程序使用 threaded 模式并不是一种好的做法。根据 cx_Oracle 文档,在单线程应用程序中将 threaded 参数设置为 true 将使性能下降 10% 到 15%。

这里要讨论一个与脚本有关的问题。执行时,它不会等到正在启动的线程完成 — 启动子线程后主线程将结束,不会等到子线程完成。如果您并不希望这样而是希望程序仅在所有线程都完成后再结束,那么您可以在脚本末尾调用每个 AsyncBlobInsert 实例的 join 方法。这将阻塞主线程,使其等待子线程的完成。对前面的脚本进行修改,使其等待 for 循环中启动的所有线程完成,如下所示:

下一节中提供了需要强制主线程等待子线程完成的示例。

同步对共享资源的访问

前面的示例显示了一个多线程的 Python 应用程序,该程序处理几个彼此并无关联的任务,因此很容易分离并放到不同的线程中进行并行处理。但是在实际中,您经常需要处理彼此相互关联的操作,并且需要在某个时刻进行同步。

作为单个进程的一部分,线程共享相同的全局内存,因此可以通过共享资源(如变量、类实例、流和文件)在彼此之间传递信息。但是,这种在线程间交换信息的简单方法是有条件的 — 当修改的对象可以同时在另一线程中访问和/或修改时,您确实要非常谨慎。因此,如果能够避免冲突,使用一个机制来同步对共享数据的访问,这将是很有用的。

为帮助解决这一问题,Python 允许您指定锁定,然后可以由某个线程取得该锁定以确保对该线程中您所使用的数据结构进行独占访问。Threading 模块附带有 Lock 方法,您可以使用该方法指定锁定。但是请注意,使用 threading.Lock 方法指定的锁定最初处于未锁定状态。要锁定一个分配的锁,您需要显式调用该锁定对象的 acquire 方法。之后,可以对需要锁定的对象执行操作。例如,当向线程中的 stdout 标准输出流进行写入时,您可能需要使用锁,以免其他使用 stdout 的线程发生重叠。进行此操作后,您需要使用锁定对象的 release 方法释放该锁,以使释放的数据结构可用于其他线程中的进一步处理。

关于锁定要注意的是,它们并不绑定到单个线程。在一个线程中指定的锁,可以由另一个线程获得,并由第三个线程释放。以下脚本例举了实际操作中的一个简单的锁。此处,为在子线程中进行使用,您在主线程中指定了一个锁,在向 DOM 文档写入之前获得它,然后立即释放。

然后,您可以使用主线程同步在各子线程中对 DOM 对象所做的更新,在主线程中调用每个子线程对象的 join 方法。之后,您可以在主流中对 DOM 文档对象进行进一步处理。在该特定示例中,您只是将其写入 stdout 标准输出流。在上面的脚本中,您首先在主线程中创建了一个文档对象模型 (DOM) 文档对象,然后在并行运行的子线程中修改该文档,添加包含从数据库获取的信息的标签。此处,您将针对 HR 演示模式中的 employees 表使用了两个简单的查询。为避免在向 DOM 对象并行写入期间发生冲突,您需要在每个子线程中获取在主线程中指定的锁。一个子线程获得该锁后,另一个子线程将无法修改此处处理的 DOM 对象,直至第一个线程释放该锁。

因此,您可能已经注意到,此处展示的示例实际上并没有讨论如何锁定数据库访问操作,例如,发出查询或针对并行线程中的同一数据库表进行更新。实际上,Oracle 数据库有自己的强大锁定机制,可确保并发环境中的数据完整性。而您的任务是正确使用这些机制。下一节中,我们将讨论如何利用 Oracle 数据库特性控制对共享数据的并发访问,从而让数据库处理并发性问题。

使 Oracle 数据库管理并发性

如上所述,当对存储在 Oracle 数据库中的共享数据进行访问或操作时,您不必在 Python 代码中手动实施资源锁定。为解决并发性问题,Oracle 数据库根据事务概念在后台使用不同类型的锁和多版本并发性控制系统。在实际操作中,这意味着,您只需考虑如何正确利用事务以确保正确访问、更新或更改数据库数据。具体来说,您必须谨慎地在自动提交事务模式和手动提交事务模式之间做出选择,将多个 SQL 语句组合到一个事务中时也需小心仔细。最后,必须避免发生并发事务间的破坏性交互。

在这里,需要记住的是,您在 Python 代码中使用的事务与连接而非游标相关联,这意味着您可以轻松地按照逻辑将使用不同游标但通过相同连接执行的语句组合到一个事务中。但是,如果您希望实施两个并发事务,则需要创建两个不同的连接对象。

在前面的“Python 中的多线程编程”一节中讨论的多线程示例中,您将连接对象的 autocommit 模式设置为 true,从而指示 cx_Oracle 模块在每个 INSERT 语句后隐式执行 COMMIT。在这种特定情况下,使用自动提交模式是合理的,因为这样可以避免子线程和主线程间的同步,从而可以在主线程中手动执行 COMMIT,如下所示:

很显然,上述两个操作必须封装到一个事务中。为此,您必须关闭 autocommit 模式,该模式为默认模式。此外,您还将需要使用主线程同步并行线程,然后显式执行 COMMIT,如上述代码段所示。但是,在有些情况下,您需要用到上述方案。考虑以下示例。假设您在两个并行线程中分别执行以下两个操作。在一个线程中,您将采购订单文档保存到数据库中,包括订单详细信息。在另一个线程中,您对包含该订单中涉及产品的相关信息的表进行修改,更新可供购买的产品数量。

虽然上述方案可以轻松实现,但在实际中,您可能最希望在数据库中实施第二个操作,即更新可供购买的产品的数量,将 BEFORE INSERT 触发器放到存储订单详细信息的表上,这样它可以自动更新包含相关产品信息的表中的相应记录。这将简化 Python 端的代码并消除编写多线程 Python 脚本的需求,让 Oracle 数据库来处理数据完整性问题。实际上,如果在放入 details 表的 BEFORE INSERT 触发器中更新产品表时出现问题,Oracle 数据库将自动回滚将新行插入到 details 表的操作。

在 Python 端,需要进行的操作仅是将用于保存订单详细信息的所有 INSERT 封装到一个事务中,如下所示:

Twisted 提供了一种不增加复杂性的编码事件驱动应用程序的好方法,使 Python 中的多线程编程更加简单、安全。Twisted 并发性模式基于无阻塞调用概念。您调用一个函数来请求某些数据并指定一个在请求数据就绪时调用的回调函数。而于此同时,程序可以继续执行其他任务。

使用 Python 事件驱动的框架 Twisted

Twisted 不随 Python 提供,需要下载并在装有 Python 的系统中安装。您可以从 Twisted Matrix Labs Web 站点http://twistedmatrix.com下载适合您 Python 版本的 Twisted 安装程序包。下载程序包之后,只需在 Twisted 设置向导中进行几次点击即可完成安装,安装大约需要一分钟的时间。

Twisted 是一个事件驱动的框架,因此,其事件循环一旦启动即持续运行,直到事件完成。在 Twisted 中,事件循环使用名为 reactor 的对象进行实施。使用 reactor.run 方法启动 Twisted 事件循环,使用 reactor.stop 停止该循环。而另一个名为 Deferred 的 Twisted 对象用于管理回调。以下是简化了的现实中的 Twisted 事件循环和回调示例。__name__ 测试用于确保解决方案将仅在该模块作为主脚本调用但不导入时(即,必须从命令行、使用 IDLE Python GUI 或通过单击图标调用该解决方案)运行。

关于上述代码最应注意的是,它在继续执行程序流的前提下,以无阻塞模式运行针对数据库发出的查询。要确保它以此方式工作,可以在对 runInteraction 的调用(runInteraction 指示 Twisted 依次对 _getBlobs 和 _writeBlobs 进行异步调用)下插入一些代码以增强 render_GET 方法。新插入的代码应使用 request.write 方法将一些内容发送回客户端,这样您可以看到,该输出出现在客户端浏览器的 _writeBlobs 中生成该输出之前。执行时,该脚本在端口 8000 启动 TCP 服务器监听。接受客户端连接后,该脚本将下载 blob_tab 数据库中存储的所有图像,并将其作为单独的文件存储在 /tmp 文件夹中,然后将相应的消息发送回客户端。要测试应用程序,您需要运行脚本,然后将浏览器指向http://localhost:8000。

结论

当下,并发性在数据密集型应用程序中频繁使用。高效使用并发性是提升应用程序性能的关键所在。编写并发应用程序最高效的一种方法是使用多线程。但是,正如您在本文中所了解到的,由于全局解释器锁 (GIL) 的原因,Python 中的多线程化对多处理器计算机没有任何好处 (GIL)。但是,当将其用于开发数据库密集型代码以及异步、事件驱动的代码时,您仍然可以受益于多线程。

本文是并发性之路的良好起点,为您提供了有价值的背景信息,有助于决策如何充分利用并发性来设计支持 Oracle 数据库的 Python 应用程序。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181203B15Y0100?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券