最近,我参与了一个客户的项目,他运行着一个大型的生产应用。这个应用已经从SQL Server“移植到”Oracle。之所以把“移植”一词用引号括起来,原因是我看到的大多数移植都只是“怎么能只对SQL Server代码做最少的改动,就让它们在Oracle上编译和执行”。要把应用从一个数据库真正移植到另一个数据库,这绝对是一项大工程。必须仔细检查算法,看看算法在目标数据库上能否正确地工作;诸如并发控制和锁定机制等特性在不同的数据库中实现不同,这会直接影响应用在不同数据库上如何工作。另外还要深入地分析算法,看看在目标数据库中实现这些算法是否合理。坦率地讲,我看到的大多数应用通常都是根据“最小移植”得来的,因为这些应用最需要帮助。当然,反过来也一样:把一个Oracle应用简单地“移植”到SQL Server,而且尽可能地避免改动,这也会得到一个很成问题、表现极差的应用。
无论如何,这种“移植”的目标都是扩缩应用,要支持更大的用户群。不过,“移植”应用的开发人员一方面想达到这个目的,另一方面又想尽量少出力。所以,这些开发人员往往保持客户和数据库层的体系结构基本不变,简单地将数据从SQL Server移到Oracle,而且尽可能不改动代码。如果决定将原来SQL Server上的应用设计原封不动地用在Oracle上,就会导致严重的后果。这种决定最糟糕的两个后果是:
q Oracle中采用与SQL Server同样的数据库连接体系结构。
q 开发人员在SQL中使用直接量(而不是绑定变量)。
这两个结果导致系统无法支持所需的用户负载(数据库服务器的可用内存会耗尽),即使用户能登录和使用应用,应用的性能也极差。
1. 在Oracle中使用一个连接
目前SQL Server中有一种很普遍的做法,就是对想要执行的每条并发语句都打开一个数据库连接。如果想执行5个查询,可能就会在SQL Server中看到5个连接。SQL Server就是这样设计的,就好像Windows是针对多线程而不是多进程设计的一样。在Oracle中,不论你想执行5个查询还是500个查询,都希望最多打开一个连接。Oracle就是本着这样的理念设计的。所以,SQL Server中常用的做法在Oracle中却不提倡;你可能并不想维护多个数据库连接。
不过,他们确实这么做了。一个简单的Web应用对于每个网页可能打开5个、10个、15个甚至更多连接,这意味着,相对于服务器原本能支持的并发用户数,现在服务器只能支持其1/5、1/10、1/15甚至更少的并发用户数。另外,开发人员只是在Windows平台本身上运行数据库,这是一个平常的Windows XP服务器,而没有使用Datacenter版本的Windows。这说明,Windows单进程体系结构会限制Oracle数据库服务器总共只有大约1.75 GB的RAM。由于每个Oracle连接要同时处理多条语句,所以Oracle连接通常比SQL Server连接占用更多的RAM(不过Oracle连接比SQL Server连接能干多了)。开发人员能否很好地扩缩应用,很大程度上受这个硬件的限制。尽管服务器上有8 GB的RAM,但是真正能用的只有2 GB左右。
注意 Windows环境中还能通过其他办法得到更多的RAM,如利用/AWE开关选项,但是只有诸如Windows Server Datacenter Edition等版本的操作系统才支持这个选项,而在这个项目中并没有使用这种版本。
针对这个问题,可能的解决方案有3种,无论哪一种解决方案都需要做大量工作(另外要记住,这可是在原先以为“移植”已经结束的情况下补充的工作!)。具体如下:
q 重建应用,充分考虑到这样一个事实:应用是在Oracle上运行,而不是在另外某个数据库上;另外生成一个页面只使用一个连接,而不是5~15个连接。这是从根本上解决这个问题的惟一方法。
q 升级操作系统(这可不是小事情),使用Windows Datacenter版本中更大的内存模型(这本身就非区区小事,而且还会带来相当复杂的数据库安装,需要一些间接的数据缓冲区和其他非标准的设置)。
q 把数据库从Windows系列操作系统移植到另外某个使用多进程的操作系统,以便数据库使用所安装的全部RAM(重申一遍,这个任务也不简单)。
可以看到,以上都不是轻轻松松就能办到的。不论哪种方法,你都不会毫无芥蒂地一口应允“好的,我们下午就来办”。每种方案都相当复杂,所要解决的问题原本在数据库“移植”阶段修正才最为容易,那是你查看和修改代码的第一个机会。另外,如果交付生产系统之前先对“可扩缩性”做一个简单的测试,就能在最终用户遭遇苦痛之前及时地捕捉到这些问题。
2. 使用绑定变量
如果我要写一本书谈谈如何构建不可扩缩的Oracle应用,肯定会把“不要使用绑定变量”作为第一章和最后一章的标题重点强调。这是导致性能问题的一个主要原因,也是阻碍可扩缩性的一个重要因素。Oracle将已解析、已编译的SQL连同其他内容存储在共享池(shared pool)中,这是系统全局区(System Global Area ,SGA)中一个非常重要的共享内存结构。第4章将详细讨论共享池。这个结构能完成“平滑”操作,但有一个前提,要求开发人员在大多数情况下都会使用绑定变量。如果你确实想让Oracle缓慢地运行,甚至几近停顿,只要根本不使用绑定变量就可以办到。
绑定变量(bind variable)是查询中的一个占位符。例如,要获取员工123的相应记录,可以使用以下查询:
![]()
或者,也可以将绑定变量:empno设置为123,并执行以下查询:
![]()
在典型的系统中,你可能只查询一次员工123,然后不再查询这个员工。之后,你可能会查询员工456,然后是员工789,如此等等。如果在查询中使用直接量(常量),那么每个查询都将是一个全新的查询,在数据库看来以前从未见过,必须对查询进行解析、限定(命名解析)、安全性检查、优化等。简单地讲,就是你执行的每条不同的语句都要在执行时进行编译。
第二个查询使用了一个绑定变量:empno,变量值在查询执行时提供。这个查询只编译一次,随后会把查询计划存储在一个共享池(库缓存)中,以便以后获取和重用这个查询计划。以上两个查询在性能和可扩缩性方面有很大差别,甚至可以说有天壤之别。
从前面的描述应该能清楚地看到,与重用已解析的查询计划(称为软解析,soft parse)相比,解析包含有硬编码变量的语句(称为硬解析,hard parse)需要的时间更长,而且要消耗更多的资源。硬解析会减少系统能支持的用户数,但程度如何不太明显。这部分取决于多耗费了多少资源,但更重要的因素是库缓存所用的闩定(latching)机制。硬解析一个查询时,数据库会更长时间地占用一种低级串行化设备,这称为闩(latch),有关的详细内容请参见第6章。这些闩能保护Oracle共享内存中的数据结构不会同时被两个进程修改(否则,Oracle最后会得到遭到破坏的数据结构),而且如果有人正在修改数据结构,则不允许另外的人再来读取。对这些数据结构加闩的时间越长、越频繁,排队等待闩的进程就越多,等待队列也越长。你可能开始独占珍贵的资源。有时你的计算机显然利用不足,但是数据库中的所有应用都运行得非常慢。造成这种现象的原因可能是有人占据着某种串行化设备,而其他等待串行化设备的人开始排队,因此你无法全速运行。数据库中只要有一个应用表现不佳,就会严重地影响所有其他应用的性能。如果只有一个小应用没有使用绑定变量,那么即使其他应用原本设计得很好,能适当地将已解析的SQL放在共享池中以备重用,但因为这个小应用的存在,过一段时间就会从共享池中删除已存储的SQL。这就使得这些设计得当的应用也必须再次硬解析SQL。真是一粒老鼠屎就能毁了一锅汤。
如果使用绑定变量,无论是谁,只要提交引用同一对象的同一个查询,都会使用共享池中已编译的查询计划。这样你的子例程只编译一次就可以反复使用。这样做效率很高,这也正是数据库期望你采用的做法。你使用的资源会更少(软解析耗费的资源相当少),不仅如此,占用闩的时间也更短,而且不再那么频繁地需要闩。这些都会改善应用的性能和可扩缩性。
要想知道使用绑定变量在性能方面会带来多大的差别,只需要运行一个非常小的测试来看看。在这个测试中,将在一个表中插入一些记录行。我使用如下所示的一个简单的表:

下面再创建两个非常简单的存储过程。它们都向这个表中插入数字1到10 000;不过,第一个过程使用了一条带绑定变量的SQL语句:

第二个过程则分别为要插入的每一行构造一条独特的SQL语句:

现在看来,二者之间惟一的差别,是一个过程使用了绑定变量,而另一个没有使用。它们都使用了动态SQL(所谓动态SQL是指直到运行时才确定的SQL),而且过程中的逻辑也是相同的。不同之处只在于是否使用了绑定变量。
下面用我开发的一个简单工具runstats对这两个方法详细地进行比较:
注意 关于安装runstats和其他工具的有关细节,请参见本书开头的“配置环境”一节。


结果清楚地显示出,从墙上时钟来看,proc2(没有使用绑定变量)插入10 000行记录的时间比proc1(使用了绑定变量)要多出很多。实际上,proc2需要的时间是proc1的3倍多,这说明,在这种情况下,对于每个“无绑定变量”的INSERT,执行语句所需时间中有2/3仅用于解析语句!
注意 如果愿意,也可以不用runstats,而是在SQL*Plus中执行命令SET TIMING ON,然后运行proc1和proc2,这样也能进行比较。
不过,对于proc2,还有更糟糕的呢!runstats工具生成了一个报告,显示出这两种方法在闩利用率方面的差别,另外还提供了诸如解析次数之类的统计结果。这里我要求runstats打印出差距在1 000以上的比较结果(这正是rs_stop调用中1000的含义)。查看这个信息时,可以看到各方法使用的资源存在显著的差别:


注意 你自己测试时可能会得到稍微不同的值。如果你得到的数值和上面的一样,特别是如果闩数都与我的测试结果完全相同,那倒是很奇怪。不过,假设你也像我一样,也是在Linux平台上使用Oracle9i Release 2,应该能看到类似的结果。不论哪个版本,可以想见,硬解析处理每个插入所用的闩数总是要高于软解析(对于软解析,更确切的说法应该是,只解析一次插入,然后反复执行)。还在同一台机器上,但是如果使用 Oracle 10g Release 1执行前面的测试,会得到以下结果:与未使用绑定变量的方法相比,绑定变量方法执行的耗用时间是前者的1/10,而所用的闩总数是前者的17%。这有两个原因,首先,10g 是一个新的版本,有一些内部算法有所调整;另一个原因是在10g中,PL/SQL采用了一种改进的方法来处理动态SQL。
可以看到,如果使用了绑定变量(后面称为绑定变量方法),则只有4次硬解析;没有使用绑定变量时(后面称为无绑定变量方法),却有不下10 000次的硬解析(每次插入都会带来一次硬解析)。还可以看到,无绑定变量方法所用的闩数是绑定变量方法的两倍之多。这是因为,要想修改这个共享结构,Oracle必须当心,一次只能让一个进程处理(如果两个进程或线程试图同时更新同一个内存中的数据结构,将非常糟糕,可能会导致大量破坏)。因此,Oracle采用了一种闩定(latching)机制来完成串行化访问,闩(latch)是一种轻量级锁定设备。不要被“轻量级”这个词蒙住了,作为一种串行化设备,闩一次只允许一个进程短期地访问数据结构。闩往往被硬解析实现滥用,而遗憾的是,这正是闩最常见的用法之一。共享池的闩和库缓存的闩就是不折不扣的闩;它们成为人们频繁争抢的目标。这说明,想要同时硬解析语句的用户越多,性能问题就会变得越来越严重。人们执行的解析越多,对共享池的闩竞争就越厉害,队列会排得越长,等待的时间也越久。
注意 如果机器的处理器不止一个,在9i 和以上版本中,共享池还可以划分为多个子池,每个子池都由其自己的闩保护。这样即使应用没有使用绑定变量,也可以提高可扩缩性,但是这并没有从根本上克服闩定问题。
执行无绑定变量的SQL语句,很像是在每个方法调用前都要编译子例程。假设把Java源代码交付给客户,在调用类中的方法之前,客户必须调用Java编译器,编译这个类,再运行方法,然后丢掉字节码。下一次想要执行同样的方法时,他们还要把这个过程再来一遍:先编译,再运行,然后丢掉字节码。你肯定不希望在应用中这样做。数据库里也应该一样,绝对不要这样做。
对于这个特定的项目,可以把现有的代码改写为使用绑定变量,这是最好的做法。改写后的代码与原先比起来,速度上有呈数量级的增长,而且系统能支持的并发用户数也增加了几倍。不过,在时间和精力投入方面却要付出很大的代价。并不是说使用绑定变量有多难,也不是说使用绑定变量容易出错,而只是因为开发人员最初没有使用绑定变量的意识,所以必须回过头去,几乎把所有代码都检查和修改一遍。如果他们从第一天起就很清楚在应用中使用绑定变量至关重要,就不用费这么大的功夫了。





