罗尹奇
(电子科技大学 图书馆,四川 成都 611731)
在面向海量数据的应用场景下,数据库越来越成为制约整个系统性能的瓶颈。为解决数据库对系统性能的制约,在设计层面上,基于一定的数据库设计原则[1]增加必要的冗余字段,减少多表之间关联查询,实现数据库表解耦;在数据库执行性能上,通过建立索引[2]、SQL优化[3-4]、配置调优等方式来提升数据库系统本身的执行效率。
然而在实际应用中,系统的开发语言与数据库的通信过程同样也对性能产生影响。为解决Java语言在MySQL数据库访问过程中产生的性能问题,本文提出采用JNI技术,将数据库访问过程交由本地动态链接库实现,从而达到提升性能的目的。
JNI(Java Native Interface)从Java1.1开始属于JDK的一部分,是Java本地应用程序接口,确保了代码在不同的平台上方便移植[5]。JNI通过调用约定,允许Java调用其他语言(如C/C++)开发的模块;同时本地模块也可以通过JNI来操作JVM内存中的Java对象,实现与Java应用程序共享数据[6]。
在频繁的数据库访问过程中,编程语言的执行效率会产生性能上的差异,且在面向海量数据的应用场景下,该差异不可忽视。通常情况下,C/C++开发的本地数据库访问模块相较于其他语言,在性能上更有优势。JNI技术则保证了Java应用程序可以调用该高速访问模块,从而获得一定的性能提升。
动态链接库DLL(Dynamic Link Library)是一种共享技术,其内部封装了一组可以被共享的例程和资源,允许其他应用程序采用动态的方式进行加载、调用和运行,从而实现某些特定的功能[7-8]。
本地数据库访问模块通常采用基于C/C++的动态链接库技术开发,其作用:一方面,对JNI定义的本地接口提供具体的实现过程,保证了Java应用程序可以调用其内部函数完成特定的功能;另一方面,Java访问数据库的过程交由本地动态链接库完成,不仅在性能上可以得到提升,同时在开发上也可以实现模块化和独立编译。
为阐述JNI技术在MySQL数据库访问过程中的应用,本文对系统接口进行了设计。根据一般的数据库应用场景,本文重点对数据库连接/断开、增删改查业务进行了实现。设计结果如图1所示:
图1 系统设计
本地接口:一组采用native关键词修饰的Java方法,仅提供方法的声明,本身不具备Java的代码实现,主要作用为应用层提供调用接口;同时可基于该接口利用JNI生成本地调用头文件。
本地实现:对JNI生成的头文件进行实现,负责完成具体的数据库访问逻辑。实现形式上采用了C/C++开发的动态链接库,将具体的访问代码进行了封装;同时由于涉及数据的跨语言传递,因此该库还需操作JVM将数据库访问结果回传至虚拟机内存。
依赖库:在本地库实现过程中,还需依赖标准库、系统库和libmysql库。MySQL数据库是平台相关的,其数据结构在不同平台下的定义有所不同,因此本地库需根据平台的不同依赖特定的系统库和libmysql库。
2.2.1 Java 本地接口
创建Java本地接口时,需要使用native关键词对方法进行修饰,方法仅做声明,无具体的代码实现。关键代码如表1所示:
在表1中,openConnection/closeConnection方法负责声明数据库的连接/断开;execute方法负责声明增、删、改操作,返回值为非0时表示操作成功,0表示操作失败;query方法负责声明查询操作,返回值为查询后的结果。
表1 Java 本地接口
2.2.2 JNI 头文件
基于2.2.1的本地接口,在项目编译后生成的bin目录下,通过javah命令生成C/C++头文件。该头文件一方面声明了Java与C/C++的调用约定,不可随意修改以防出现无法调用的情况;另一方面本地动态链接库需要对该头文件进行实现,提供具体的数据库访问代码。关键代码如表2所示:
表2 C/C++头文件
需要注意的是,生成的头文件中包含了JNI定义的数据类型,在本地动态链接库开发时还需引入jni.h和jni_md.h头文件。
2.2.3 本地实现
对2.2.2生成的头文件进行实现时,需要包含标准头文件、系统头文件和MySQL头文件。各头文件作用如表3所示:
表3 包含的头文件
由于Java本地接口仅做连接/断开操作,不对数据库连接对象进行访问,因此在本地动态链接库中还需对数据库连接对象进行维护。为了方便起见,本文采用了全局变量的方式来维护数据库连接对象,同时包括加载libmysql.dll的句柄。关键代码如表4所示:
表4 全局变量声明
在实现数据库连接/断开时,本文采用了动态加载动态链接库技术,利用Windows系统函数获取/释放句柄;同时在使用libmysql.dll库中的函数时,采用了函数指针来获取库函数的调用地址,从而实现MySQL函数的调用。关键代码如表5、表6所示:
表5 数据库连接
表6 数据库断开
在表5中全局句柄仅加载一次,表6中不对句柄进行释放,从而防止在大量数据库连接/断开场景中,频繁加载释放全局句柄引发内存崩溃。而针对数据库连接指针,在打开连接的时候动态分配内存空间,在关闭连接的时候不仅需要执行断开操作,同时也要执行内存释放,并将指针置空,从而保证内存不会发生泄漏。
数据库的读写操作主要包括了数据的增删改查四个功能,execute方法完成数据的写入,即增删改功能;query方法完成数据的读取,即查询功能,并且将查询的结果返回给Java应用。关键代码如表7、表8所示:
表7 数据写入
表8 数据读取
在表8中,由于查询结果需要从C/C++的本地动态链接库回传给Java应用,因此需要利用JNI环境指针实现对虚拟机操作。
首先在虚拟机环境中并不存在ArrayList对象,因此需要获取ArrayList类型和构造函数,通过NewObject在虚拟机中创建一个ArrayList对象。后续的Object[]数组的创建同理。
其次MYSQL_ROW类型本质上是char**类型,为保证数据能够正确地传递回Java,在提取某个字段的数据时(即one_row[index]),其类型为char*,因此将其转化为字符串String类型传递给Java,再在Java应用中将String类型转化为其所需的数据类型。故MySQL中不论是什么类型的数据(如Varchar、Datetime、Blob等)均按照字符串处理,回传给Java应用后再自行决定类型转化。
最后由于Java本身是支持多态的,String数据可以赋值给Object引用,故在向Object[]数组添加String数据时不会引发虚拟机错误。
为了测试本地动态链接库对Java应用访问MySQL数据库的性能提升,本文设计了两组对比实验,分析性能优化的程度。对比实验条件如表9所示:
表9 对比实验条件
为保证实验准确性,实验组与对照组均访问相同的数据库和表,执行相同的SQL语句,且通过多次重复执行SQL并取平均时间来显示性能的变化。结果如图2所示:
图2 性能对比
图2结果显示,在数据库连接/断开和数据库读写性能对比上,基于JNI的本地动态链接库均具备性能上的优势,JNI技术对Java访问MySQL数据库的性能有一定提高。
综上所述,本文通过JNI技术定义了Java本地访问接口,采用C/C++生成动态链接库对本地接口进行实现,并采用两组对比实验论证了JNI对数据库连接/断开和读写均有性能上的提升。这对从编程语言层面上,改进Java应用访问MySQL数据库的性能提供了一定的借鉴。
然而上述方案中仍存在一定的缺陷,一方面Java应用通常是基于JDBC接口进行数据库访问,自定义的本地接口不具备通用性,Java应用需要修改代码才能使用,无法做到直接替换底层驱动库而不用修改代码的目的;另一方面本文采用了全局数据库连接对象,在多线程应用中无法创建多个数据库连接。针对上述问题,在后续的工作中还需做进一步研究。