羅尹奇
(電子科技大學(xué) 圖書館,四川 成都 611731)
在面向海量數(shù)據(jù)的應(yīng)用場(chǎng)景下,數(shù)據(jù)庫(kù)越來越成為制約整個(gè)系統(tǒng)性能的瓶頸。為解決數(shù)據(jù)庫(kù)對(duì)系統(tǒng)性能的制約,在設(shè)計(jì)層面上,基于一定的數(shù)據(jù)庫(kù)設(shè)計(jì)原則[1]增加必要的冗余字段,減少多表之間關(guān)聯(lián)查詢,實(shí)現(xiàn)數(shù)據(jù)庫(kù)表解耦;在數(shù)據(jù)庫(kù)執(zhí)行性能上,通過建立索引[2]、SQL優(yōu)化[3-4]、配置調(diào)優(yōu)等方式來提升數(shù)據(jù)庫(kù)系統(tǒng)本身的執(zhí)行效率。
然而在實(shí)際應(yīng)用中,系統(tǒng)的開發(fā)語言與數(shù)據(jù)庫(kù)的通信過程同樣也對(duì)性能產(chǎn)生影響。為解決Java語言在MySQL數(shù)據(jù)庫(kù)訪問過程中產(chǎn)生的性能問題,本文提出采用JNI技術(shù),將數(shù)據(jù)庫(kù)訪問過程交由本地動(dòng)態(tài)鏈接庫(kù)實(shí)現(xiàn),從而達(dá)到提升性能的目的。
JNI(Java Native Interface)從Java1.1開始屬于JDK的一部分,是Java本地應(yīng)用程序接口,確保了代碼在不同的平臺(tái)上方便移植[5]。JNI通過調(diào)用約定,允許Java調(diào)用其他語言(如C/C++)開發(fā)的模塊;同時(shí)本地模塊也可以通過JNI來操作JVM內(nèi)存中的Java對(duì)象,實(shí)現(xiàn)與Java應(yīng)用程序共享數(shù)據(jù)[6]。
在頻繁的數(shù)據(jù)庫(kù)訪問過程中,編程語言的執(zhí)行效率會(huì)產(chǎn)生性能上的差異,且在面向海量數(shù)據(jù)的應(yīng)用場(chǎng)景下,該差異不可忽視。通常情況下,C/C++開發(fā)的本地?cái)?shù)據(jù)庫(kù)訪問模塊相較于其他語言,在性能上更有優(yōu)勢(shì)。JNI技術(shù)則保證了Java應(yīng)用程序可以調(diào)用該高速訪問模塊,從而獲得一定的性能提升。
動(dòng)態(tài)鏈接庫(kù)DLL(Dynamic Link Library)是一種共享技術(shù),其內(nèi)部封裝了一組可以被共享的例程和資源,允許其他應(yīng)用程序采用動(dòng)態(tài)的方式進(jìn)行加載、調(diào)用和運(yùn)行,從而實(shí)現(xiàn)某些特定的功能[7-8]。
本地?cái)?shù)據(jù)庫(kù)訪問模塊通常采用基于C/C++的動(dòng)態(tài)鏈接庫(kù)技術(shù)開發(fā),其作用:一方面,對(duì)JNI定義的本地接口提供具體的實(shí)現(xiàn)過程,保證了Java應(yīng)用程序可以調(diào)用其內(nèi)部函數(shù)完成特定的功能;另一方面,Java訪問數(shù)據(jù)庫(kù)的過程交由本地動(dòng)態(tài)鏈接庫(kù)完成,不僅在性能上可以得到提升,同時(shí)在開發(fā)上也可以實(shí)現(xiàn)模塊化和獨(dú)立編譯。
為闡述JNI技術(shù)在MySQL數(shù)據(jù)庫(kù)訪問過程中的應(yīng)用,本文對(duì)系統(tǒng)接口進(jìn)行了設(shè)計(jì)。根據(jù)一般的數(shù)據(jù)庫(kù)應(yīng)用場(chǎng)景,本文重點(diǎn)對(duì)數(shù)據(jù)庫(kù)連接/斷開、增刪改查業(yè)務(wù)進(jìn)行了實(shí)現(xiàn)。設(shè)計(jì)結(jié)果如圖1所示:
圖1 系統(tǒng)設(shè)計(jì)
本地接口:一組采用native關(guān)鍵詞修飾的Java方法,僅提供方法的聲明,本身不具備Java的代碼實(shí)現(xiàn),主要作用為應(yīng)用層提供調(diào)用接口;同時(shí)可基于該接口利用JNI生成本地調(diào)用頭文件。
本地實(shí)現(xiàn):對(duì)JNI生成的頭文件進(jìn)行實(shí)現(xiàn),負(fù)責(zé)完成具體的數(shù)據(jù)庫(kù)訪問邏輯。實(shí)現(xiàn)形式上采用了C/C++開發(fā)的動(dòng)態(tài)鏈接庫(kù),將具體的訪問代碼進(jìn)行了封裝;同時(shí)由于涉及數(shù)據(jù)的跨語言傳遞,因此該庫(kù)還需操作JVM將數(shù)據(jù)庫(kù)訪問結(jié)果回傳至虛擬機(jī)內(nèi)存。
依賴庫(kù):在本地庫(kù)實(shí)現(xiàn)過程中,還需依賴標(biāo)準(zhǔn)庫(kù)、系統(tǒng)庫(kù)和libmysql庫(kù)。MySQL數(shù)據(jù)庫(kù)是平臺(tái)相關(guān)的,其數(shù)據(jù)結(jié)構(gòu)在不同平臺(tái)下的定義有所不同,因此本地庫(kù)需根據(jù)平臺(tái)的不同依賴特定的系統(tǒng)庫(kù)和libmysql庫(kù)。
2.2.1 Java 本地接口
創(chuàng)建Java本地接口時(shí),需要使用native關(guān)鍵詞對(duì)方法進(jìn)行修飾,方法僅做聲明,無具體的代碼實(shí)現(xiàn)。關(guān)鍵代碼如表1所示:
在表1中,openConnection/closeConnection方法負(fù)責(zé)聲明數(shù)據(jù)庫(kù)的連接/斷開;execute方法負(fù)責(zé)聲明增、刪、改操作,返回值為非0時(shí)表示操作成功,0表示操作失敗;query方法負(fù)責(zé)聲明查詢操作,返回值為查詢后的結(jié)果。
表1 Java 本地接口
2.2.2 JNI 頭文件
基于2.2.1的本地接口,在項(xiàng)目編譯后生成的bin目錄下,通過javah命令生成C/C++頭文件。該頭文件一方面聲明了Java與C/C++的調(diào)用約定,不可隨意修改以防出現(xiàn)無法調(diào)用的情況;另一方面本地動(dòng)態(tài)鏈接庫(kù)需要對(duì)該頭文件進(jìn)行實(shí)現(xiàn),提供具體的數(shù)據(jù)庫(kù)訪問代碼。關(guān)鍵代碼如表2所示:
表2 C/C++頭文件
需要注意的是,生成的頭文件中包含了JNI定義的數(shù)據(jù)類型,在本地動(dòng)態(tài)鏈接庫(kù)開發(fā)時(shí)還需引入jni.h和jni_md.h頭文件。
2.2.3 本地實(shí)現(xiàn)
對(duì)2.2.2生成的頭文件進(jìn)行實(shí)現(xiàn)時(shí),需要包含標(biāo)準(zhǔn)頭文件、系統(tǒng)頭文件和MySQL頭文件。各頭文件作用如表3所示:
表3 包含的頭文件
由于Java本地接口僅做連接/斷開操作,不對(duì)數(shù)據(jù)庫(kù)連接對(duì)象進(jìn)行訪問,因此在本地動(dòng)態(tài)鏈接庫(kù)中還需對(duì)數(shù)據(jù)庫(kù)連接對(duì)象進(jìn)行維護(hù)。為了方便起見,本文采用了全局變量的方式來維護(hù)數(shù)據(jù)庫(kù)連接對(duì)象,同時(shí)包括加載libmysql.dll的句柄。關(guān)鍵代碼如表4所示:
表4 全局變量聲明
在實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接/斷開時(shí),本文采用了動(dòng)態(tài)加載動(dòng)態(tài)鏈接庫(kù)技術(shù),利用Windows系統(tǒng)函數(shù)獲取/釋放句柄;同時(shí)在使用libmysql.dll庫(kù)中的函數(shù)時(shí),采用了函數(shù)指針來獲取庫(kù)函數(shù)的調(diào)用地址,從而實(shí)現(xiàn)MySQL函數(shù)的調(diào)用。關(guān)鍵代碼如表5、表6所示:
表5 數(shù)據(jù)庫(kù)連接
表6 數(shù)據(jù)庫(kù)斷開
在表5中全局句柄僅加載一次,表6中不對(duì)句柄進(jìn)行釋放,從而防止在大量數(shù)據(jù)庫(kù)連接/斷開場(chǎng)景中,頻繁加載釋放全局句柄引發(fā)內(nèi)存崩潰。而針對(duì)數(shù)據(jù)庫(kù)連接指針,在打開連接的時(shí)候動(dòng)態(tài)分配內(nèi)存空間,在關(guān)閉連接的時(shí)候不僅需要執(zhí)行斷開操作,同時(shí)也要執(zhí)行內(nèi)存釋放,并將指針置空,從而保證內(nèi)存不會(huì)發(fā)生泄漏。
數(shù)據(jù)庫(kù)的讀寫操作主要包括了數(shù)據(jù)的增刪改查四個(gè)功能,execute方法完成數(shù)據(jù)的寫入,即增刪改功能;query方法完成數(shù)據(jù)的讀取,即查詢功能,并且將查詢的結(jié)果返回給Java應(yīng)用。關(guān)鍵代碼如表7、表8所示:
表7 數(shù)據(jù)寫入
表8 數(shù)據(jù)讀取
在表8中,由于查詢結(jié)果需要從C/C++的本地動(dòng)態(tài)鏈接庫(kù)回傳給Java應(yīng)用,因此需要利用JNI環(huán)境指針實(shí)現(xiàn)對(duì)虛擬機(jī)操作。
首先在虛擬機(jī)環(huán)境中并不存在ArrayList對(duì)象,因此需要獲取ArrayList類型和構(gòu)造函數(shù),通過NewObject在虛擬機(jī)中創(chuàng)建一個(gè)ArrayList對(duì)象。后續(xù)的Object[]數(shù)組的創(chuàng)建同理。
其次MYSQL_ROW類型本質(zhì)上是char**類型,為保證數(shù)據(jù)能夠正確地傳遞回Java,在提取某個(gè)字段的數(shù)據(jù)時(shí)(即one_row[index]),其類型為char*,因此將其轉(zhuǎn)化為字符串String類型傳遞給Java,再在Java應(yīng)用中將String類型轉(zhuǎn)化為其所需的數(shù)據(jù)類型。故MySQL中不論是什么類型的數(shù)據(jù)(如Varchar、Datetime、Blob等)均按照字符串處理,回傳給Java應(yīng)用后再自行決定類型轉(zhuǎn)化。
最后由于Java本身是支持多態(tài)的,String數(shù)據(jù)可以賦值給Object引用,故在向Object[]數(shù)組添加String數(shù)據(jù)時(shí)不會(huì)引發(fā)虛擬機(jī)錯(cuò)誤。
為了測(cè)試本地動(dòng)態(tài)鏈接庫(kù)對(duì)Java應(yīng)用訪問MySQL數(shù)據(jù)庫(kù)的性能提升,本文設(shè)計(jì)了兩組對(duì)比實(shí)驗(yàn),分析性能優(yōu)化的程度。對(duì)比實(shí)驗(yàn)條件如表9所示:
表9 對(duì)比實(shí)驗(yàn)條件
為保證實(shí)驗(yàn)準(zhǔn)確性,實(shí)驗(yàn)組與對(duì)照組均訪問相同的數(shù)據(jù)庫(kù)和表,執(zhí)行相同的SQL語句,且通過多次重復(fù)執(zhí)行SQL并取平均時(shí)間來顯示性能的變化。結(jié)果如圖2所示:
圖2 性能對(duì)比
圖2結(jié)果顯示,在數(shù)據(jù)庫(kù)連接/斷開和數(shù)據(jù)庫(kù)讀寫性能對(duì)比上,基于JNI的本地動(dòng)態(tài)鏈接庫(kù)均具備性能上的優(yōu)勢(shì),JNI技術(shù)對(duì)Java訪問MySQL數(shù)據(jù)庫(kù)的性能有一定提高。
綜上所述,本文通過JNI技術(shù)定義了Java本地訪問接口,采用C/C++生成動(dòng)態(tài)鏈接庫(kù)對(duì)本地接口進(jìn)行實(shí)現(xiàn),并采用兩組對(duì)比實(shí)驗(yàn)論證了JNI對(duì)數(shù)據(jù)庫(kù)連接/斷開和讀寫均有性能上的提升。這對(duì)從編程語言層面上,改進(jìn)Java應(yīng)用訪問MySQL數(shù)據(jù)庫(kù)的性能提供了一定的借鑒。
然而上述方案中仍存在一定的缺陷,一方面Java應(yīng)用通常是基于JDBC接口進(jìn)行數(shù)據(jù)庫(kù)訪問,自定義的本地接口不具備通用性,Java應(yīng)用需要修改代碼才能使用,無法做到直接替換底層驅(qū)動(dòng)庫(kù)而不用修改代碼的目的;另一方面本文采用了全局?jǐn)?shù)據(jù)庫(kù)連接對(duì)象,在多線程應(yīng)用中無法創(chuàng)建多個(gè)數(shù)據(jù)庫(kù)連接。針對(duì)上述問題,在后續(xù)的工作中還需做進(jìn)一步研究。