數據庫訪問性能優化(三)

3.4、使用存儲過程

大型數據庫一般都支持存儲過程,合理的利用存儲過程也可以提高系統性能。如你有一個業務需要將A表的數據做一些加工然後更新到B表中,但是又不可能一條SQL完成,這時你需要如下3步操作:

a:將A表數據全部取出到客戶端;

b:計算出要更新的數據;

c:將計算結果更新到B表。

 

如果採用存儲過程你可以將整個業務邏輯封裝在存儲過程裏,然後在客戶端直接調用存儲過程處理,這樣可以減少網絡交互的成本。

當然,存儲過程也並不是十全十美,存儲過程有以下缺點:

a、不可移植性,每種數據庫的內部編程語法都不太相同,當你的系統需要兼容多種數據庫時最好不要用存儲過程。

b、學習成本高,DBA一般都擅長寫存儲過程,但並不是每個程序員都能寫好存儲過程,除非你的團隊有較多的開發人員熟悉寫存儲過程,否則後期系統維護會產生問題。

c、業務邏輯多處存在,採用存儲過程後也就意味着你的系統有一些業務邏輯不是在應用程序裏處理,這種架構會增加一些系統維護和調試成本。

d、存儲過程和常用應用程序語言不一樣,它支持的函數及語法有可能不能滿足需求,有些邏輯就只能通過應用程序處理。

e、如果存儲過程中有複雜運算的話,會增加一些數據庫服務端的處理成本,對於集中式數據庫可能會導致系統可擴展性問題。

f、爲了提高性能,數據庫會把存儲過程代碼編譯成中間運行代碼(類似於javaclass文件),所以更像靜態語言。當存儲過程引用的對像(表、視圖等等)結構改變後,存儲過程需要重新編譯才能生效,在24*7高併發應用場景,一般都是在線變更結構的,所以在變更的瞬間要同時編譯存儲過程,這可能會導致數據庫瞬間壓力上升引起故障(Oracle數據庫就存在這樣的問題)

 

個人觀點:普通業務邏輯儘量不要使用存儲過程,定時性的ETL任務或報表統計函數可以根據團隊資源情況採用存儲過程處理。

 

3.5、優化業務邏輯

要通過優化業務邏輯來提高性能是比較困難的,這需要程序員對所訪問的數據及業務流程非常清楚。

舉一個案例:

某移動公司推出優惠套參,活動對像爲VIP會員並且2010123月平均話費20元以上的客戶。

那我們的檢測邏輯爲:

select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';

select vip_flag from member where phone_no='13988888888';

if avg_money>20 and vip_flag=true then

begin

  執行套參();

end;

 

如果我們修改業務邏輯爲:

select avg(money) as  avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';

if avg_money>20 then

begin

  select vip_flag from member where phone_no='13988888888';

  if vip_flag=true then

  begin

    執行套參();

  end;

end;

通過這樣可以減少一些判斷vip_flag的開銷,平均話費20元以下的用戶就不需要再檢測是否VIP了。

 

如果程序員分析業務,VIP會員比例爲1%,平均話費20元以上的用戶比例爲90%,那我們改成如下:

select vip_flag from member where phone_no='13988888888';

if vip_flag=true then

begin

  select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';

  if avg_money>20 then

  begin

    執行套參();

  end;

end;

這樣就只有1%VIP會員纔會做檢測平均話費,最終大大減少了SQL的交互次數。

 

以上只是一個簡單的示例,實際的業務總是比這複雜得多,所以一般只是高級程序員更容易做出優化的邏輯,但是我們需要有這樣一種成本優化的意識。

 

3.6、使用ResultSet遊標處理記錄

現在大部分Java框架都是通過jdbc從數據庫取出數據,然後裝載到一個list裏再處理,list裏可能是業務Object,也可能是hashmap

由於JVM內存一般都小於4G,所以不可能一次通過sql把大量數據裝載到list裏。爲了完成功能,很多程序員喜歡採用分頁的方法處理,如一次從數據庫取1000條記錄,通過多次循環搞定,保證不會引起JVM Out of memory問題。

 

以下是實現此功能的代碼示例,t_employee表有10萬條記錄,設置分頁大小爲1000

 

d1 = Calendar.getInstance().getTime();

vsql = "select count(*) cnt from t_employee";

pstmt = conn.prepareStatement(vsql);

ResultSet rs = pstmt.executeQuery();

Integer cnt = 0;

while (rs.next()) {

         cnt = rs.getInt("cnt");

}

Integer lastid=0;

Integer pagesize=1000;

System.out.println("cnt:" + cnt);

String vsql = "select count(*) cnt from t_employee";

PreparedStatement pstmt = conn.prepareStatement(vsql);

ResultSet rs = pstmt.executeQuery();

Integer cnt = 0;

while (rs.next()) {

         cnt = rs.getInt("cnt");

}

Integer lastid = 0;

Integer pagesize = 1000;

System.out.println("cnt:" + cnt);

for (int i = 0; i <= cnt / pagesize; i++) {

         vsql = "select * from (select * from t_employee where id>? order by id) where rownum<=?";

         pstmt = conn.prepareStatement(vsql);

         pstmt.setFetchSize(1000);

         pstmt.setInt(1, lastid);

         pstmt.setInt(2, pagesize);

         rs = pstmt.executeQuery();

         int col_cnt = rs.getMetaData().getColumnCount();

         Object o;

         while (rs.next()) {

                   for (int j = 1; j <= col_cnt; j++) {

                            o = rs.getObject(j);

                   }

                   lastid = rs.getInt("id");

         }

         rs.close();

         pstmt.close();

}

 

以上代碼實際執行時間爲6.516

 

很多持久層框架爲了儘量讓程序員使用方便,封裝了jdbc通過statement執行數據返回到resultset的細節,導致程序員會想採用分頁的方式處理問題。實際上如果我們採用jdbc原始的resultset遊標處理記錄,在resultset循環讀取的過程中處理記錄,這樣就可以一次從數據庫取出所有記錄。顯著提高性能。

這裏需要注意的是,採用resultset遊標處理記錄時,應該將遊標的打開方式設置爲FORWARD_READONLY模式(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY),否則會把結果緩存在JVM裏,造成JVM Out of memory問題。

 

代碼示例:

 

String vsql ="select * from t_employee";

PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);

pstmt.setFetchSize(100);

ResultSet rs = pstmt.executeQuery(vsql);

int col_cnt = rs.getMetaData().getColumnCount();

Object o;

while (rs.next()) {

         for (int j = 1; j <= col_cnt; j++) {

                   o = rs.getObject(j);

         }

}

調整後的代碼實際執行時間爲3.156

 

從測試結果可以看出性能提高了1倍多,如果採用分頁模式數據庫每次還需發生磁盤IO的話那性能可以提高更多。

iBatis等持久層框架考慮到會有這種需求,所以也有相應的解決方案,在iBatis裏我們不能採用queryForList的方法,而應用該採用queryWithRowHandler加回調事件的方式處理,如下所示:

 

MyRowHandler myrh=new MyRowHandler();

sqlmap.queryWithRowHandler("getAllEmployee", myrh);

 

class MyRowHandler implements RowHandler {

    public void handleRow(Object o) {

       //todo something

    }

}

 

iBatisqueryWithRowHandler很好的封裝了resultset遍歷的事件處理,效果及性能與resultset遍歷一樣,也不會產生JVM內存溢出。

 

當一條SQL發送給數據庫服務器後,系統首先會將SQL字符串進行hash運算,得到hash值後再從服務器內存裏的SQL緩存區中進行檢索,如果有相同的SQL字符,並且確認是同一邏輯的SQL語句,則從共享池緩存中取出SQL對應的執行計劃,根據執行計劃讀取數據並返回結果給客戶端。

如果在共享池中未發現相同的SQL則根據SQL邏輯生成一條新的執行計劃並保存在SQL緩存區中,然後根據執行計劃讀取數據並返回結果給客戶端。

爲了更快的檢索SQL是否在緩存區中,首先進行的是SQL字符串hash值對比,如果未找到則認爲沒有緩存,如果存在再進行下一步的準確對比,所以要命中SQL緩存區應保證SQL字符是完全一致,中間有大小寫或空格都會認爲是不同的SQL

如果我們不採用綁定變量,採用字符串拼接的模式生成SQL,那麼每條SQL都會產生執行計劃,這樣會導致共享池耗盡,緩存命中率也很低。

 

一些不使用綁定變量的場景:

a、數據倉庫應用,這種應用一般併發不高,但是每個SQL執行時間很長,SQL解析的時間相比SQL執行時間比較小,綁定變量對性能提高不明顯。數據倉庫一般都是內部分析應用,所以也不太會發生SQL注入的安全問題。

b、數據分佈不均勻的特殊邏輯,如產品表,記錄有1億,有一產品狀態字段,上面建有索引,有審覈中,審覈通過,審覈未通過3種狀態,其中審覈通過9500萬,審覈中1萬,審覈不通過499萬。

要做這樣一個查詢:

select count(*) from product where status=?

採用綁定變量的話,那麼只會有一個執行計劃,如果走索引訪問,那麼對於審覈中查詢很快,對審覈通過和審覈不通過會很慢;如果不走索引,那麼對於審覈中與審覈通過和審覈不通過時間基本一樣;

對於這種情況應該不使用綁定變量,而直接採用字符拼接的方式生成SQL,這樣可以爲每個SQL生成不同的執行計劃,如下所示。

select count(*) from product where status='approved'; //不使用索引

select count(*) from product where status='tbd'; //不使用索引

select count(*) from product where status='auditing';//使用索引


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章