设想这样一种情形:你突然口渴,需要一杯水来缓解,从心情上来讲,当然是越快越好了。通常,一杯水的产生包括从水源(井水、河水或江水、甚至海水等)抽取,通过管道传输和设备净化,才到达你饮水的容器中。上述过程是必须的,但并不是每一杯水的产生都必须把上述过程重复一次。你可以用一个大一点的容器(例如缸或罐等)来盛大量的水,喝水之前分到杯子小部分中即可,你的代价只是把水从缸转移到杯子;你还可以在大量用水(例如洗澡或洗衣服等)时,只需打开水阀,而不必临时铺设通往水源的管道和购买净化水的设备。因为水是人们生活不可缺少的东西,每时每刻都在被大量地使用,而且物理本质也完全相同,所以政府会铺设管道和建设水处理站,完成那些比较困难的工作,达到资源共享的目的,而你也可针对自己的需求,用容器来盛那些具有特定用途的水。本文将要和你讨论的高速缓存和连接池与上述特定容器和传输管道有很多相似之处,它们都达到了同一个目的:在满足用户意愿的前提下,尽可能地共享资源, 以提高整个系统的性能。
高速缓存和连接池是数据访问中的重要技术,某些情况下的应用对访问数据库的性能有巨大的提高,而且都得到了数据库业界的普遍支持。前者由 DBMS 厂商针对自己的数据库实现,提供可供用户配置的方案;后者是 JDBC 的一个标准接口,由支持 J2EE 技术的应用服务器厂商提供具体的实现,而你的 Java 程序代码无需更改。本文将向你简单介绍高速缓存和连接池的概念和机制,并以 PointBase 数据库为例向你展示高速缓存的应用,而一个简单的连接池应用场景将向你描述应用的条件和提高的性能。
Cache(高速缓存)和 Connection Pool(连接池)的概念和机制
它们不是数据库独有的技术,但却得到数据库业界的普遍支持,并在其它数据存取和对象复用领域有很多类似的应用。
Cache(高速缓存)
作为个人计算机的日常使用者,你肯定听说过这些名词:Cache(高速缓存)、Memory(内存)、Hard disk(硬盘)。它们都是数据存取单元,但存取速度却有很大差异,呈依次递减的顺序。对于 CPU 来说,它可以从距离自己最近的 Cache 高速地存取数据,而不是从内存和硬盘以低几个数量级的速度来存取数据。而 Cache 中所存储的数据,往往是 CPU 要反复存取的数据,有特定的机制(或程序)来保证 Cache 内数据的命中率(Hit Rate)。因此,CPU 存取数据的速度在应用高速缓存后得到了巨大的提高。
对于数据库来说,厂商的做法往往是在内存中开辟相应的区域来存储可能被多次存取的数据和可能被多次执行的语句,以使这些数据在下次被访问时不必再次提交对 DBMS 的请求和那些语句在下次执行时不必再次编译。
因为将数据写入高速缓存的任务由 Cache Manager 负责,所以对用户来说高速缓存的内容肯定是只读的。需要你做的工作很少,程序中的 SQL 语句和直接访问 DBMS 时没有分别,返回的结果也看不出有什么差别。而数据库厂商往往会在 DB Server 的配置文件中提供与 Cache 相关的参数,通过修改它们,可针对我们的应用优化 Cache 的管理。下图是在 Win2K 中配置 MS Access 数据源的界面,在"驱动程序"部分你可设置的页超时和缓冲区大小就是和 Cache 有关的参数。在后面的讨论中,我将展示一个更复杂的数据库,向你解释 Cache 对访问数据库性能的影响和如何寻找最优的配置方案。
Connection Pool(连接池)
池是一个很普遍的概念,和缓冲存储有机制相近的地方,都是缩减了访问的环节,但它更注重于资源的共享。下图展示了建立"调制解调器池"以共享调制解调器资源的 VPN 拨号方案:
对于访问数据库来说,建立连接的代价比较昂贵,因此,我们有必要建立"连接池"以提高访问的性能。我们可以把连接当作对象或者设备,池中又有许多已经建立的连接,访问本来需要与数据库的连接的地方,都改为和池相连,池临时分配连接供访问使用,结果返回后,访问将连接交还。
JDBC 1.0 标准及其扩展中没有定义连接池,而在 JDBC 2.0 标准的扩展中定义了与连接池相关的接口。与接口对应的类由应用服务器厂商实现,你可在对服务器的管理过程中调节某个数据库连接池的参数。下图简略地描述了连接池的运行机制:
在 PointBase 数据库 DB Server 的配置参数列表中,我们可以找到这几个参数:cache.checkpointinterval、cache.size、SQLCaching.size 等。下表是对各个参数的描述:
| 参数名 | 参数描述 |
| cache.checkpointinterval | 检查点的时间间隔 |
| cache.size | 高速缓存的最大页数(素数时,性能最好) |
| SQLCaching.size | 高速缓存中 SQL 语句的个数 |
对于 cache.checkpointinterval 来说,和前面 Access 界面中的页超时是一个概念,它指定了页面内容更新的时间间隔,这取决于你的应用对时效性的要求程度。
对于 cache.size 来说,指定了页面的个数,一般应设定为符合你查询结果的需求。至于为何是素数,我也纳闷,不过还是遵照厂商的指示吧。
对于 SQLCaching.size 来说,指定了存储的经过编译的 SQL 语句的个数,你可以把它设定为 0,从而取消这个选项。
使用 Cache 后,性能到底有多大提高?我打开 PointBase 的 Console 通过 JDBC 驱动访问 Server,将 SQL 菜单下的 Timing Mode 选上,以显示各个步骤的耗费时间。执行语句是:
SELECT * FROM PRODUCT_TBL |
第一次访问,总计耗时 1082 毫秒,而编译耗时 771 毫秒。
紧接着的第二次访问,总计耗时仅为 160 毫秒,而编译耗时为 0。
再接着的第三次访问,总计耗时仅为 91 毫秒,编译耗时也为 0。
关闭 Console,等待超过 30 秒之后,重新开启 Console,执行相同的语句,总计耗时 210 毫秒,编译耗时为 20 毫秒。
自等待超过 30 秒之后,执行语句,总计耗时 101 毫秒,编译耗时为 0。
由此可以看出,高速缓存的应用大大提高了访问数据库的性能,而其参数的设定则要依据前面对它们的描述来进行,需要你仔细阅读数据库的配置文档。
我选择 IBM 公司的应用服务器平台 WebSphere 来给大家演示连接池的设置,使你面对友好的 Web 界面,可以体验到非常简易的操作场景。
首先,我们进入 WebSphere 的管理控制台,这是一个非常漂亮的 Web 界面:
紧接着,我选定一个数据源:Session Persistence datasource,就可看到这个数据源的属性配置了。在这儿,仅仅列举和连接池有关的属性:
| Minimum Pool Size | 池中保持的连接的最小数目;有新的请求,且没有激活连接可供使用时,池中连接数将增大,到最大连接数为止 |
| Maximum Pool Size | 池中保持的连接的最大数目;当这个数目达到,且没有激活连接可供使用时,新的请求将等待 |
| Connection Timeout | 当连接数达到最大值,且激活连接都在被使用时,新的请求等待的时间 |
| Idle Timeout | 连接可在池中闲置的时间;超过将释放资源,到最小连接数为止 |
| Orphan Timeout | 连接在被应用控制时,可闲置的时间;超过将返回池中 |
你可以根据需要来修改这些数值,以满足你的应用需要。接下来,我们讨论一下连接池的应用。
EJB 访问数据库(场景 1,使用 JDBC 1.0)
import java.sql.*;
import javax.sql.*;
...
public class AccountBean implements EntityBean {
...
public Collection ejbFindByLastName(String lName) {
try {
String dbdriver = new initialContext().lookup("java:comp/env/DBDRIVER")
.toString();
Class.forName(dbdriver).newInstance();
Connection conn = null;
conn = DriverManager.getConnection("java:comp/env/DBURL", "userID", "password");
...
conn.close();
}
...
}
|
如果 EntityBean 是一个共享组件,那么每次客户请求时,都要建立和释放与数据库的连接,这成为影响性能的主要问题。
EJB 访问数据库(场景 2,使用 JDBC 2.0)
import java.sql.*;
import javax.sql.*;
// import here vendor specific JDBC drivers
public ProductPK ejbCreate() {
try {
// initialize JNDI lookup parameters
Context ctx = new InitialContext(parms);
...
ConnectionPoolDataSource cpds = (ConnectionPoolDataSource)ctx.lookup(cpsource);
...
// Following parms could all come from a JNDI look-up
cpds.setDatabaseName("PTDB");
cpds.setUserIF("XYZ");
...
PooledConnection pc = cpds.getPooledConnection();
Connection conn = pc.getConnection();
...
// do business logic
conn.close();
}
...
}
|
EJB 组件利用 JNDI 的 lookup() 方法定位数据库的连接池资源,利用 getConnection() 方法得到已经打开的数据库连接,而用 close() 来释放连接,放回池中。因此,与场景 1 相比,少了与数据库建立物理连接的损耗。对于原本要频繁打开和关闭物理连接的应用来说,通过这种建立逻辑连接并复用的方法,性能肯定能够得到大幅度提高。
性能问题并不局限于数据库的应用上,而是存在于每个软件系统中。我们希望软件系统付出的最小,而获得的最大,因而无时无刻不在优化它们。通过《 Java 程序访问数据库的速度瓶颈问题的分析和解决》和本文,我对 Java 程序访问数据库的性能问题做了分析,并提供了优化 Java 程序的解决方案,希望对你有所帮助。
也许你会关心碰到类似的性能问题时应如何分析和解决,我就针对此次探讨"访问数据库的速度瓶颈"问题的过程中碰到的难题、网友的意见和自己的体会,作一个关于"方法论"的经验总结,希望能够抛砖引玉,更好地解决类似的问题。
- 不要让硬件的低配置成为软件正常运行的障碍,后者有升级前者的需求,请立即满足;经常碰到这样的问题"P166+64M 的机子跑 Win2K+MySql+JBoss,能跑么?"我在回答"可以"的同时只有对着屏幕发呆了。
- 尽量使用商业软件,并享受良好的售后技术支持;如果你没有黑客精神,请不要使用自由软件。
- 分析好自己的问题,也许它的本质和他人的不同;没有一把钥匙可打开的任一把锁。
- 确定瓶颈环节的位置;解决了瓶颈问题,往往整个的性能问题就解决了,千万不要抓着边缘的问题不放。
- 将瓶颈环节细分为多个顺序的流程,用逐个替代的方法来试探瓶颈的核心位置;细分问题使你都问题有更进一步的了解。
- 做自己能做的和该做的事情,始终面向自己的现实问题;不要尝试那些应该由厂商解决的问题(例如,自己写个 JDBC 驱动)。
- 他人的方案只供自己参考,解决要靠自己思考;你我应用的情形不同,应用解决方案要在理解他人的方案之后。
- 观察新技术,应用新技术;它往往包含了前人对问题解决的思路,只是对你来说不可见。
- 问题得到解决后,立即罢手,并汇报结果;在现实问题得到解决后,没有必要花费精力在非核心的问题上,也许它们永远不会被碰到,不要假想问题让自己解决。
上述经验为个人即兴的总结,并未有严谨的逻辑推导,仅代表我解决技术问题的思路,供你碰到类似问题时参考。
可以提前告诉大家的是,下一篇文章我将把主题转移到 JDO 的介绍和讨论上。这个陌生的 API 已经进入 Final Draft 了,可它到底能够给数据存取带来什么好处,和 JDBC 有什么关联的地方,《 JDO(Java Data Object)的发展和介绍》将向你娓娓道来。
- BBS 水木清华站(telnet://smth.org)
- Java World,Dive into connection pooling with J2EE(
http://www.javaworld.com/javaworld/jw-10-2000/jw-1027-pool.html)
- PointBase(
http://www.pointbase.com)
- WebSphere 开发者园地(
http://www.ibm.com/websphere/developer/cn)
- Java 程序访问数据库的速度瓶颈问题的分析和解决,
FoolsGarden@SMTH