提高JDBC应用程序的性能

本文探讨了JDBC驱动程序的四种模式,强调在数据库应用程序设计中选择合适模式的重要性。对于大型企业开发,选择模式3是提高性能的关键,而模式1则可能不适用于此类场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JDBC是Java 数据库连接 (Java Database Connectivity) API,也是目前Java访问数据库的核心部分。这里,我们不准备太多的去复述JDBC的发展历史以及它的各种特性。我们尝试从开发人员关心的性能问题去讨论JDBC,同时也介绍几个提高JDBC性能的基本策略和方法。笔者在自己的数据库访问程序中使用了这些方法,从而提升了程序的性能。

: 选择合适的JDBC 驱动程序模式,并作为程序设计的考虑因素之一

到目前为止,JDBC驱动程序有4种模式。选择何种模式主要取决于程序的应用范围。正确选择合适的模式,使之符合于数据库程序的设计,是提高程序性能必须考虑的一个方面。这里,我们给出JDBC驱动程序的4种模式的简要说明:


模式


工作机制

说 明

1

2

JDBC-ODBC

JDBC 操作翻译成对应的 ODBC 调用。

2

2

本机 API/ 集团式 Java 驱动

JDBC 操作翻译成针对特定数据库的调用。

3

3

网络协议 / Java 驱动

JDBC 操作翻译成网络协议并转发给中间层服务器。

4

2

本级协议 / Java 驱动

JDBC 操作直接转换成不使用 ODBC 或本机 API 的本机协议。

(表 1)



模式

优点

缺点

1

因为多数 RDBMS 平台都支持 ODBC 驱动程序 , 所以使用 Jdbc-Odbc 桥能与大量 ODBC 驱动程序协同工作。

1 :用户受底层 ODBC 驱动程序的功能限制。

2 ODBC 需要在每个客户端得到配置。

3 :不能用于 applet , 因为 applet 不能加载本地调用。

2

不需要转换成 Odbc 调用,比模式 1 的性能要好得多。

使用 Java 本地接口,平台移植性不好。

3

广泛适用于 Internet/Intranet 的开发,安全性和性能都十分显著。

进行数据库操作时,需要花费较长的时间。

4

由于是通过本机协议访问数据库,所以性能很高。

用户需要给不同的数据库使用不同的 JDBC 驱动程序。

(表 2)

 

建议:当采用模式1时,由于它需要首先把JDBC操作翻译成对应的ODBC调用,然后这些调用又被传递给ODBC驱动程序,最后才执行数据库的相关操作,很显然它的性能要大打折扣。所以,在2层结构中,为了提高程序的性能,我们最好还是用模式2的驱动程序。

说明2:对于进行大型企业开发部署的数据库开发人员来说采用模式1作为设计基础,那简直就是噩梦。在企业应用中选择模式3才是明智的。由于笔者的经验有限,这里暂时不对基于模式3的情况进行分析。


小结:关于不同模式驱动程序的选择,其利弊以及和程序性能三者之间的联系,我们这里就不多说了。虽然模式1有诸多不好,但在后面的分析中我们仍然以模式1作为我们的实验环境。

 

: 在面向对象的程序设计和提升程序性能之间做出平衡


当我们设计数据库应用程序的结构时,经常需要把对象模型映射成数据库中的表,或者把表反向映射成对象模型。面向对象的一个基本原则是: 使得每一个对象与真实世界中的对应物具有共同的特征,以符合我们的思维习惯。


 比如,数据库中有这样一张学生表:






Id


Name


Department


Position


Address


1


张三


物理系


团员


昆明



 现在,我们需要把该表从数据库中映射给用户,基于上述原则,我们考虑封装一个类 (在这我们暂时就称它为Record Set,当然具体到对学生表的映射,指的就是Student类 ),并且得出一个遵循面向对象设计原则的映射方案:

(图3――方案1)


我们知道,数据库应用程序的一个主要功能就是要把从数据库中查询到的结果,以某种形式再现给用户。为了符合用户的习惯,我们的程序使用了Java Swing 包中的JTable作为用户和程序之间交互的接口。我们现在唯一要做的就是确保程序映射数据的准确与快速.


我们使用AbstractTableModel 关于AbstractTableModel 和 JTable的说明可以查看JDK1.3的Doc 作为映射数据的接口,当然也是用户与程序交互的核心。 我们知道一个表格模型可以抽象为一个二维对象数组,或者二维 Vector 这样的数据结构。于是,我们有了另一个基于数据结构的映射方案:


                                (图4――方案2)


那究竟选择哪种方法呢?通过下表的比较,我们不难得出结论。

                                   (表 4)


说明1 方案1表面上似乎遵循了面向对象的原则,但是:


(1):其工作量显然比方案2大。


(2):结果集因为类封装而失去了灵活性。


(3):中间的各种转化会使程序的性能降低。


说明2 但是方案1 在很多情况下都是最佳的选择。因为,当用户通过Web方式与程序交互时,实现对结果集的再封装就是必要的了。


小结 在笔者的程序中,为了提高程序的性能,不得以放弃了面向对象的程序设计原则。而以 JTable AbstractTableModel API 的本身特性反向得出了映射方案。有时依赖于 API 提供的方法,往往能提高程序的性能。当然这样的选择,主要取决于实际的要求和整个系统的结构设计,我们应该从不同的角度去考虑,最后才能做出平衡。
 
 
: 优化数据库连接
在优化Java编写的程序时,对象重用是一个经常使用的方法。其中关键的一点,就是要尽量的重用一个已有的对象,而不是反复的创建一个新的对象。这样不仅能减少对内存的消耗,也降低了程序因为不断创建新对象而导致内存溢出的情况。由于建立一个数据库连接或者撤销一个连接都是代价昂贵的操作,所以重用一个Connection就是理所当然的了。我们有两种方式重用一个Connection:
 
方法
方法说明
使用环境
1
为每个用户建立一个单独的连接,并反复使用该连接创建语句 , 执行相关的数据库操作。
每个用户必须经过验证才能使用该连接。
2
建立连接池
多用户
(表 5)
我们使用完一个连接后一定要将其关闭,否则可能会因为连接资源耗尽,而导致程序性能降低,甚至使程序崩溃。
   关闭联接有以下四种方式:
(1):不关闭连接
    (2): // ……
Connection con = DriverManager.getConnection(url,user,password);
con = null;
       // ……
    (3): // ……
Connection con = DriverManager.getConnection(url,user,password);
con.close();//显示的关闭连接
      // ……
    (4):// ……
Connection con = DriverManager.getConnection(url,user,password);
con.close();//显示的关闭连接
con = null;
       // ……
 
下面我们给出一些测试数据,通过比较我们不难得出关闭连接的正确方式:
 首先我们给出测试程序,我们创建100个线程,每个线程都各自获得1个数据库连接。这里的每个线程就好比一个希望连接数据库的用户。
 
TestJdbc.java
import java.sql.Connection;
import java.sql.DriverManager;
public class TestJdbc {
    public TestJdbc() {           
    }
    public void connect(){
      try{
          Class.forName("sun.jdbc.odbc.JdbcOdbcDriver").newInstance();
     }
     catch(Exception ex){
        ex.printStackTrace();
    }
       //创建100个线程
      for(int i=0;i<100;i++){
        MakeConnection makeConnect = new MakeConnection();
        makeConnect.setName(""+i+"");
        makeConnect.start();
      }
    }
   
   public static void main(String args[]){    
     TestJdbc test = new TestJdbc();
     test.connect();       
  }
   
class MakeConnection extends Thread {
   public MakeConnection () {
    }
    //创建1个连接
    public void run(){
        try  {    Connection con =null;
                  //关闭连接的方式,可以是上述4种方式之一
                  con = DriverManager.getConnection("jdbc:odbc:test");
                  con.close();
                  con = null;
        }  
       catch(Exception e){
            e.printStackTrace();
       }
    }
  }
 测试一:Java堆内存分配最小为0Kb,最大为2000Kb
 
 
方式 1
方式 2
方式 3
方式 4
预期创建连接数目 ( )
100
100
100
100
创建及关闭连接耗时 ( 毫秒 )
6,540
7,850
20,320
13,950
实际生成连接实例数 ( )
100
100
100
100
剩余连接实例数 ( )
26
24
24
23
创建连接消耗内存量 ( 字节 )
8,800
8,800
8,800
8,718
剩余连接占用内存量 ( 字节 )
2,288
2,112
2,112
2,024
                                      (表 6)
    从测试一的数据我们看不出四种方式有什么太大的差别,这是因为Java堆分配的太小,当程序的内存消耗过大时,垃圾回收线程就自动回收那些无用的对象实例,从而影响了我们的测试结果。我们尝试分配一个更大的Java堆,以排除垃圾回收机制对测试结果的影响。
测试二:Java堆内存分配最小为0Kb,最大为20,000Kb
 
方式 1
方式 2
方式 3
方式 4
预期创建连接数目 ( )
100
100
100
100
创建及关闭连接耗时 ( 毫秒 )
6,650
6,430
9,280
9,610
实际生成连接实例数 ( )
63
65
100
100
剩余连接实例数 ( )
63
65
100
100
创建连接消耗内存量 ( 字节 )
5,544
5,720
8,800
8,800
剩余连接占用内存量 ( 字节 )
5,544
5,720
8,800
8,800
(表 7)
 
说明: (1) 剩余连接实例数是指没有被垃圾回收的连接数。      
(2) ( 7)剩余连接实例数与实际生成连接实例数相同,说明在程序运行期间都没有进行 垃圾回收。
     从(表7)中我们看出方式1和方式2实际生成连接数小于预期创建连接数目,可能是因为没有显示的关闭连接造成的。考虑到垃圾回收的不确定性,我们最好采用方式3或者方式4来显示的关闭连接,这样做是最可靠的方法。
 
 
四:使用预编译语句和批量更新
首先我们得大致的了解数据库是怎么处理各种数据库操作语句的。当数据库接收到一个语句时,数据库引擎首先解析该语句,然后分析是否有语法,语义错误。如果没有错误,数据库将计算出如何高效的执行该语句。一旦得出执行策略,就由数据库引擎执行该语句,最后把执行结果反馈给用户。虽然数据库厂商对各自的数据库做了最大的优化,但是可以想象这的确是一个开销很大的工作。
于是,我们考虑如何使我们的数据库操作变得更高效呢?如果一条语句执行一次后,数据库就记录下该语句的执行策略,那么以后执行相同语句时,就可以省去上面的种种麻烦了。
Java里提供了这样的接口――PreparedStatement.。通过预编译PreparedStatement 对象, 我们能够很容易的提高语句执行效率。同时,需要指出的是Java里还有另一个实现数据库操作的接口――Statement,但是当语句格式固定时我们更倾向于使用PreparedStatement,只有当语句格式无法预见时,我们才考虑采用Statement。
以下是执行1000次语句结构相同的Insert,Update和Select语句的测试结果:
 
接口类型
Insert语句
Update语句
Select语句
第一次测试耗时
第二次测试耗时
第一次测试耗时
第二次测试耗时
第一次测试耗时
第二次测试耗时
Statement
2360 ms
2190 ms
3790 ms
3460 ms
3570 ms
2530 ms
PreparedStatement
1270 ms
1040 ms
2600 ms
2410 ms
440 ms
380 ms
                                     (表8)
分析: PreparedStatement的效率明显比Statement要高出很多。另外,对于查询语句我们还得深入地看看JDBC是如何实现的。JDBC执行一次查询后,将返回一个ResultSet(结果集)。为了建立这个结果集,JDBC将对数据库访问两次。第一次要求数据库对结果集中的各列进行说明,第二次告诉数据库,当程序需要获取数据时应如何安置这些数据。由此我们能够算出执行一次或多次查询,JDBC需要访问数据库的次数。
访问数据库次数 = 结果集中的列数 * 语句执行的次数 * 2
 
如果同样执行 100次相同查询,结果集中的列数也相同时,假设为20列:
使用 Statement:  访问数据库次数 = 20 * 100 * 2 = 4000   
使用Prepared Statement:  访问数据库次数 = 20 * 1* 2 = 400
 
 
以下是相关的测试结果 :

方式
Select语句
执行100次语句结构相同的查询耗时
执行1000次语句结构相同的查询耗时
第一次测试
第二次测试
第一次测试
第二次测试
方式1
1100 ms
330 ms
3510 ms
3020 ms
方式2
110 ms
50 ms
440 ms
380 ms

                                   (表9)
分析:测试结果说明,如果不正确的使用了PreparedStatement接口,那么其执行效率和使用Statement没有什么差别,而PreparedStatement接口的优势也不会得到充分发挥。
最后我们还得补充一点,当我们需要成批插入或者更新记录时。我们考虑采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率。如果我们再配合使用PreparedStatement接口,将进一步提高程序的性能。我们同样给出一个小程序予以说明。
五:通过对象引用,重用结果集

    当程序开发人员为提高程序性能而绞尽脑汁时,我们首先得想一想在程序中是否使用了缓冲技术。在大量的基于C/S模式的应用程序中,我们可以从两个不同的角度使用它。从客户端的角度,我们可以把数据缓冲到本地机,从而避免进行相同的网络操作;从服务器端的角度,我们也可以缓冲那些经常使用的数据,从而缩短服务器对相同请求的响应时间。合理地使用缓冲技术能有效的提高程序性能。

   经常出现这样的情况,在Java编程中一个或多个对象实例在程序运行期间被反复的使用。更具体的说,在JDBC编程中,用户经常使用某几条查询语句进行查询,每次执行查询都返回相同的结果集,这个时候我们就得考虑:那些反复使用的结果集是否应该被缓冲起来?如何使用这些结果集?用户更新数据时,如何保证这些结果集也能随之更新?

这里,我们将具体的讨论JDBC编程中如何实现对结果集的缓冲。首先,我们要澄清如下几个概念。

(1):在Java编程中如果要使用类,就得把类实例化成对象。类实例化成对象后,就要分配一块相应的内存空间存放该实例。通过对象引用,可以对这块内存空间进行操作。

(2):在JDBC中,执行查询操作返回的结果集,其实是一个指向数据库的连接,通过游标操作可以把需要的数据从数据库调入Java堆。我们用图表进一步说明:

 (3):对结果集的缓冲实际上是对Java堆中指定的结果集对象进行缓冲,更确切的说应该是缓冲该对象的引用。

基于上述原理,我们决定使用java util 包中的Hashtable实现对结果集对象的缓冲。我们用查询语句本身作为该结果集对象的标志,也就是Hashtable的key字段, value字段则存放了一个Vector对象。此Vector存放了ResultSet的引用“指针”(暂且称为指针);Statement或者PreparedStatement的引用“指针”;结果集的记录数;如果该结果集具有可滚动的特性(JDBC2.0 API的新特性),则还可以存放当前游标的位置。存放的内容可以根据实际情况调整,但是ResultSet的引用“指针”和 Statement的引用“指针”是必须保存的。具体结构如下图所示:

 但是,我们发现当内存空间很大,并且需要缓存的对象不是太多时,使用Hashtable的确是最省事的办法。可是,随着需缓存的对象数目的不断增加,Hashtable占用的内存空间也越来越大。为了,减少对内存的消耗,同时又能够实现缓存对象的目的。我们决定设置一个上限,当缓存对象的数目超过该上限时则把最不常用的ResultSet替换出来。下图是强化了的缓存结构:

 

(注:该数据结构以及LRU算法在jbuilder6.0的Database Pilot代码包里有一定介绍。)

当然为了降低系统消耗,使用的替换算法不能太复杂。另外,可以使用ArrayList和Hashmap来替代Vector和Hashtable,这样还能提高一点效率。但是ArrayList和Hashmap都是线程不安全的,而Vector和Hashtable却刚好相反。当我们建立的缓存需要支持多线程访问时,使用ArrayList和Hashmap就显得难以控制。所以这里我们牺牲效率以提高程序的稳定性是有意义的。

最后还得说明一点,当从缓存中删除一个对象实例时。应按如下步骤进行:

1.         使用close()方法关闭ResultSet(结果集)。

2.         使用close()方法关闭创建该结果集的语句(Statement,PreparedStatement)。

3.         删除Hasttable中的相应key及其value。

4.         从Vector中删除key.

 我们还要注意一点,如果使用PreparedStatement接口的方法不当,则不能达到提高执行效率的目的。我们用一个简单的测试程序说明:
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值