數據導入功能在報表項目中是經常可見的,因爲它是報表數據展示的基礎,但對於大量數據的導入,真正從性能、效率等方面兼顧的方案卻很少有。最近在南航廣西數據服務平臺的項目開發中,我需要設計一個能快速將40多萬條數據導入Oracle數據庫的方案,爲了實現導入的高效,我通過在網上收集資料以及動手實踐測試,得出了一些分析總結與大家分享探討。
談到數據導入功能的實現,無可厚非應該包括兩個過程,首先是數據文件的上傳,其次是數據的導入。
一、數據文件上傳
文件上傳本應該是與導入無關,但它處於數據導入功能的一個環節,其效率也顯得有些重要。對於傳統項目,文件上傳通常採用Struts等框架實現的文件上傳機制以及一些開源的文件上傳組件,比如SmartUpload等,通過Html中類型爲File的Input標籤將數據文件獲取,通過流的形式發送服務端,最後由服務端獲取流並寫入文件,如此實現了文件從客戶端到服務器的上傳過程,這些方式我們都可以將其統一稱爲Web文件上傳。
Web文件上傳是通過Http協議實現的文件流傳輸,其受限於Web數據傳輸的瓶頸,基於Http協議傳輸的數據在傳輸的速度上有一定影響,首先可能出現數據文件請求超時需要數據重傳;其次在每秒能傳輸的字節數在Web方式裏受到限制;最後,由於Web在傳輸數據文件之前需要將數據文件轉化爲數據流,通過Web的File標籤實現的文件流化效率很低,通過對比試驗發現,同樣對一個50M的文件流化,用Web的File標籤流化的時間要大於採用IO方式流化所消耗的時間,也就是說,對於大數據文件,要將其通過Web標籤流化再傳輸,可能會有很長一段時間是處於發送請求狀態,甚至會因爲這個過程的時間持續較長而造成網絡請求超時。這可能是優酷等類似的網站上傳視頻不採用Web方式的一個原因。
除了Web方式,可以考慮使用Applet,作爲一個客戶端小程序嵌入到網頁中,以IO的方式讀取本地數據文件,然後通過Socket將文件流發送到服務端。這種方式從效率上比Web方式有明顯的改進,首先是通過IO將文件轉化爲文件流的效率提升,其次數據通過Socket方傳輸式,是一種基於TCP協議的網絡傳輸,去除了Web下Http協議對文件傳輸的限制,通過TCP協議直接從網絡的傳輸層進行數據通信,傳輸速度上必然會更快。綜上,採用Applet加Socket實現網絡文件上傳性能優於Web方式。
然而,Applet實現的網絡文件上傳又並非最優,原因在於Applet在運行時受到沙箱的限制。出於對客戶機和服務器的保護,web中的applet程序只能運行在限制的沙箱中,其受到很多安全策略的限制,在applet中不能直接訪問客戶端本地文件系統,除非使用applet授權,採用數字簽名的方式使applet能確認該客戶端系統是可信的。這樣一來,要使用該功能的客戶機系統都需要安裝安全證書,在項目的部署上就顯得十分繁瑣。
以上方式都各有弊端,最終,通過和用戶協商,我們決定藉助外部的FTP工具,使用開源的文件傳輸工具讓用戶將數據文件直接傳到服務器指定目錄下,在網站系統上就只執行數據文件列表加載。另外,目前也有在web上嵌入FTP功能的插件,其通過activeObject的形式嵌入web,實現類似ftp的文件上傳功能,打算抽空繼續研究。
二、數據導入
大數據導入Oracle數據庫是功能實現的重點。
大數據導入的特點在於數據記錄多,數據插入需要批量執行,分批量commit,以此來減少對數據庫的交互訪問次數,減輕數據庫壓力。在此,我主要探索了兩種導入方式,一種是基於多線程的併發插入,另一種是利用SqlLoader實現的大數據導入。
(一)基於多線程的併發插入
該方案是在批量到插入的基礎之上採用多線程來執行的方式實現的。
該方案第一步是加載所需的數據文件到內存,生成一個Sql的數組。對於導入的數據文件,一般是EXCEL格式的,對於此種類型數據文件,我們需要藉助POI來實現EXCEL文件的加載,並通過POI讀取EXCEL中行數據來生成數據插入Sql,文件代碼如下:
private XSSFSheet get07SheetForExcel(File file, String sheetName) { if (file != null) { try { FileInputStream fileInputStream = new FileInputStream(file); // 創建對Excel工作簿文件的引用 XSSFWorkbook workbook = new XSSFWorkbook(fileInputStream); // 創建對工作表的引用�1�7�1�7 XSSFSheet sheet = workbook.getSheetAt(0); // 也可用getSheetAt(int index)按索引引用, // 在Excel文檔中,第一張工作表的缺省索引是0$1�7 // 其語句爲:HSSFSheet sheet = workbook.getSheetAt(0); return sheet; } catch (Exception e) { } } return null; }
…… for (int i = begin; null != sheet && i < sheet.getLastRowNum() - sheet.getFirstRowNum() + 1; i++) { HSSFRow row = sheet.getRow(i); System.out.println(i); String[] valuesPerRow = getHSSFRowValues(row); if (null == valuesPerRow || valuesPerRow.length < 1) { continue; } rows.add(valuesPerRow); if (rows.size() == CommonParas.PER_IMPORT_SIZE) { v.importToDB(rows); rows.clear(); } } v.importToDB(rows); |
這兩個分別是POI加載Excel文件方法和讀取文件行的數據的片段,從這裏暴露出POI處理大數據EXCEL的問題所在。首先,是將EXCEL文件加載到XSSFSheet對象的一個過程需要消耗很多時間及內存資源,通過測試得出,當EXCEL數據記錄在6萬以上,文件大小超過30M時,此處就會出現異常“JVM內存溢出”,原因就是在加載EXCEL文件時開銷太大了,即使我們在啓動服務器時擴大JVM內存分配,這也只是治標不治本的方法;其次,在循環讀取EXCEL行數據時需要預先獲得EXCEL中總行數,而獲得總行數的方法則是通過函數sheet.getLastRowNum() - sheet.getFirstRowNum()+ 1 實現,在此又出現了另一個問題,sheet.getLastRowNum()返回值是一個int類型的數據,其最大值只可能是65535,那麼它又如何可以計算出超過65535行記錄的行數呢?所以得出結論:對於大數據的讀取,不能採用POI讀取EXCEL的方式。
不採用EXCEL作爲數據源文件,可以採用CSV文件代之。CSV是EXCEL可另存爲的數據文件格式,其本質上是以逗號分隔的文本文件,因此,對於此類文件的讀取,我們可以採用傳統IO讀取文件的形式,通過字符串分割獲得每個單元格數據,拼接到SQL裏面,形成SQL的數組。
第二步,多線程執行數據導入。
第一步已經將SQL儲存到SQL的數組中,接下來就是利用多線程執行批量插入。此處實現需要將SQL執行類實現Runnable接口或者繼承Thread類,在常量中設置批量插入的記錄數,當加載的SQL數據大小達到該數目時,系統就自動啓動一個線程執行該數組的批量數據導入。爲了確保線程管理的安全,我們需要定義一個最大線程數,當處於運行的線程數達到最大值時,新開啓的線程將利用wait()方法實現等待,等待有線程執行完畢後纔開始執行。如此調用,直到加載的數據記錄全部都已開始導入才停止線程的啓動,此時爲了監控數據導入是否全部完成,我們會再新開啓一個後臺線程,設置爲守護進程setDaemon(true),該類線程特點是在所有線程結束後將自動結束,其用於收集每個導入線程的執行狀態,當所分配的所有線程狀態都不是active時就表示數據導入完成。
利用該方案實現的數據導入較單線程執行的批量數據導入效率提高多倍,從測試導入40萬數據結果來看,單線程批量導入耗時19分鐘,而基於多線程的導入只用了5分鐘左右的時間。但從性能消耗上來看,多線程方案平均同時工作線程數爲15個左右,CPU利用率高達90%,內存消耗約500M,對於服務器本身已造成了一定的壓力,雖然在速度上提升了,其對於服務器的穩定性將造成安全隱患。
此外,對於多線程工作效率的探索上也有一點心得。多線程的出現更多的是迎合多核處理技術的革新,在單CPU工作的主機上,多線程看起來貌似是多個線程併發執行,但從操作系統的角度出發其仍然處於串行狀態,因爲在同一時間,處理器只對一個任務進行調度,只不過是輪詢的時間間隙較短不容易發覺。如果在多核處理的主機上,就會有多個處理器同時處理併發的線程,這樣才能實現真正意義上的併發調度,所以多線程還是依賴於硬件本身。爲了驗證效率,當我們把執行導入的各個線程以webService的形式部署到不同的虛擬機中去執行時,效果就不一樣了,效率明顯還會提升。由此引出一個當今IT行業的一個熱點,虛擬化技術的實現與應用,有利於資源的優化配置,在有限的資源上實現更大的利用價值,該技術在雲計算領域也是頗受關注的。
(二)利用SQLLOADER執行大數據導入
SQLLOADER是oralce內置的一個命令工具,其可實現將CSV數據文件或者文本數據文件一次性導入數據庫中。它的使用需要有兩個文件,一個是需要導入的數據文件,另一個是控制導入的控制文件。該方案的執行需要三個步驟,第一是格式化數據文件,第二是生成控制文件,最後是執行導入命令。
第一,格式化數據文件。
數據文件不是已經上傳了,爲什麼需要格式化呢?原因在於SQLLOADER對於導入數據文件要求的嚴格性。利用SQLLOADER導入的數據文件有如下要求:
1、數據行之間要有合法換行標誌,數據與數據之間需要有合法分隔符,一般情況下數據之間用逗號分隔,數據記錄之間用換行符分隔;
2、要導入的數據是直接導入到數據庫的內容,不能包含表頭信息,對於數據文件中有表頭的需要將其去除;
3、數據中不能包含既定的數據分隔符,比如逗號,因爲其在導入時對於數據個數不匹配的將視爲錯誤數據被拒絕。
因此,爲了確保數據能夠準確的被導入到數據庫,在數據導入之前我們需要驗證原數據文件,並讀取其數據信息生成格式化的新數據文件提供導入。這也許是這種導入方式繁瑣的地方,但出於安全又不得不做。
第二,生成控制文件。
SQLLOADER執行數據導入並非直接通過命題操縱數據文件,而是通過一個控制文件規定了導入數據的方式,包括數據文件路徑及導入數據字段等等,它通過執行該控制文件來實現數據的導入。下面來看一個控制文件內容。
load data infile 'e:\test\艙位數據源(20120101-20120331).csv' (1) append into table CZDS_CABIN (2) fields terminated by ',' (3) ( CARRIER, (4) ORGCITY, ARRIVALCITY, FLYDATE "to_date(:FLYDATE,'YYYY-MM-DD')", FLIGHTNO, FLIGHTSEG, AIRLINE, AIRLINETOANDFROM, FLIGHTTYPE, ORGUNITS, MOTHERCABIN, SONSPACE, DISCOUNTFACTOR, CLASSES, FLIGHTNAVIGATIONSECTION, BOARDINGTIMES, GROUPTICKETRNUMBER, REVENUEFORECASTING, SEATINGSECTOR, SEATINGNAVIGATIONSECTION, SEATINGSONTNSECTION ) |
(1)定義需要導入的格式化數據文件路徑,可以是CSV文件或者TXT文本文件;
(2)定義導入數據庫的方式爲append,需要插入數據的表名爲CZDS_CABIN;
(3)定義數據間的分隔符爲逗號,此處,還可以定義更多的控制命令,比如是否允許插入空數據命令trailing nullcols等等。
(4)定義導入數據的對應的字段。這裏所列的字段要與數據文件中每行數據中數據對應的列一致,因爲SQLLOADER是按照這個順序對數據進行插入的。其次,還有個值得注意的地方,在列名的後面可以用雙引號定義導入後數據的格式。SQLLOADER導入數據時區分格式的,當插入數字和字符類型數據時可以根據文本進行自動轉換,但是當插入數據是時間類型時,需要在此處定義格式轉換,利用to_date方法將字符轉化爲日期再執行插入。同時,利用這個控制,可以實現對插入數據的格式化,比如利用lpad進行左填充等等,這也是SQLLOADER相對靈活的地方。
第三步,通過SQLLOADER命令執行數據導入。
這裏的SQLLOADER命令是一個命令行命令,需要在JAVA程序中執行CMD程序來調用。
Process process = null; try { process = java.lang.Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } |
Cmd就是需要執行的SQLLOADER命令。SQLLOADER命令如下:
sqlldr userid=mrl/mrl123456 control=e:\\test\\control1.txt log=aa.log bad=bad.bad direct=true parallel=true
Userid指的是本地數據庫的用戶名和密碼,不正確將連接不上數據庫。
Control的值是我們之前生成的控制文件路徑,命令將會執行該控制文件。
Log:這裏定義了導入的日誌文件路徑。
Bad:當導入失敗時,該文件裏存放了沒有成功導入的數據記錄。
Direct:開啓直接導入路徑模式。
Parallel:開啓併發導入模式。
在程序中執行此命令後,通過process.waitFor()方法來等待命令執行的返回,若返回值爲0則說明程序正常結束,數據導入成功;如返回1,則說明數據導入中出現異常,需要排查日誌文件,數據導入失敗。
利用SQLLOADER執行數據導入的缺點在於其對數據文件格式要求的嚴格以及其只能導入本地數據庫,而無法執行遠程導入,且導入數據只能通過閃存回滾,存在風險性。但其優點又很明顯,導入數據速度非常快,通過測試結果顯示,導入40萬數據僅用了7-8秒,較之前額方法可謂一個飛躍,這也許是其贏得大家一致認可的原因所在。
篇末,通過這次對數據導入功能實現方案的探索,我學習到了很多知識和技巧,對於數據導入的實現原理也有了自己的認識,也希望能和大家探討交流。