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

3.4、使用存儲過程

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

a:將A表數據所有取出到客戶端;程序員

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

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';//使用索引

相關文章
相關標籤/搜索