標籤:
上一篇博文中講述了使用EF開發電商項目的代碼基礎篇,提到EF後,一語激起千層浪。不少園友紛紛表示:EF不適合增長速度飛快的互連網項目,EF只適合企業級應用等等。
也有部分高手提到了分布式,確實,效能最佳化從資料庫出發,初期就加索引,然後垂直分割,水平分割,讀寫分離,甚至是分散式交易,陽春白雪,格局很高。然而筆者希望通過漸進的過程來最佳化這個項目,我們縮小格局,從細節查看不同方案的優劣。
之前提過,使用EF最主要的原因是項目時間緊迫,EF搭建速度快,熟悉的同事也多,使用方便。這個決策確實協助我們挺過了初期的難關。在業務量增長的過程中,一些問題也逐漸暴露出來,我們開始針對問題做最佳化。
問題1:部分請求響應緩慢,影響使用者體驗。
使用EF做資料的增刪改查,一些不規範代碼也會拖慢程式效率,筆者在上一篇中已經提過。某些請求中可能包含多次資料查詢與更新,如果這些細小的問題都以低效運行,那這個請求確實會很慢。然而在EF的架構下最佳化它,也未必能收到明顯成效。以更新商品銷量為例,我們上方案與代碼:
方案1
先查出某一條資料,然後填入新算出的銷量,再更新資料庫,代碼如下:
//先查出資料,再更新 var productSKU = unitOfWork.ProductSku.GetByID(dtos[0].Items[0].SKUId); productSKU.SalesCount = productSKU.SalesCount + dtos[0].Items[0].Quantity; unitOfWork.ProductSku.Update(productSKU); unitOfWork.Submit();
其回應時間
方案2
採用IQuryable,之前提到過,這種查詢方式不會真的將全部資料載入到記憶體,代碼如下:
//2採用IQueryable查詢更新 var productSKU1 = unitOfWork.ProductSku.Get(p => p.Id == dtos[0].Items[0].SKUId); foreach (var item in productSKU1) { item.SalesCount = item.SalesCount + dtos[0].Items[0].Quantity; unitOfWork.ProductSku.Update(item); } unitOfWork.Submit();
其回應時間
方案3
直接使用SQL,簡單粗暴,代碼如下:
//3直接使用sql更新 string updateSql = @"update ProductSKU set SalesCount=SalesCount+" + dtos[0].Items[0].Quantity + " where Id=‘" + dtos[0].Items[0].SKUId.ToString() + "‘"; unitOfWork.ProductSku.ExecuteSqlCommand(updateSql); unitOfWork.Submit();
其回應時間
我們來分析下這三種方案:
方案1簡單易懂,將資料查出來,更新後再塞回資料庫,邏輯清晰,代碼更清晰。可是將資料取出來,載入到記憶體,再對記憶體裡的資料進行修改,最後產生SQL送回資料庫執行,這套走下來,好像兜了一大圈,其實我們只想更新個銷量。
方案2顯得好了些,使用了我們在前一篇中講到的理念,不將資料整個載入到記憶體,只到用時才載入,通過這種方式更新,其實產生的SQL和直接執行SQL差不太多了,但是我們可以發現:方案2和方案1時間差不多。在筆者預期中,方案2應該是比方案1好的,至於時間差不多的原因,應該是方案2用了一次迴圈,第二次迴圈時不符合條件,然後跳出迴圈,造成了些許時間浪費。如果是批次更新銷量,方案2必然比方案1優秀很多。
方案3自然是簡單粗暴,一條SQL搞定,毫不拖泥帶水,其效果也是喜人的,幾乎比方案1快1倍!
之前有園友提到索引的問題,其實我廠有專職DBA,不僅為資料量較大的表添加了叢集索引和非叢集索引,還寫了定時任務去更新索引。所以索引這塊我們不深究。
早起追逐開發效率階段,我們可能將方案1最佳化為方案2,然後繼續做新功能開發。但是現在我們有更多的選擇,我們也有更多的時間去深究使用者體驗與系統效率了,那麼自然的,我們開始嫌棄EF了。但是開發到這個程度,再去更換架構似乎不合適。而且大道至簡,如果能用SQL搞定,那必是極好的。於是乎,我們開始對關鍵區段業務代碼做重構,替換為原生SQL。這似乎是拋棄EF的開端。
問題2:部分資料量很大的表需要分表,EF難以維繫
EF是ORM架構,映射資料庫物件,然而同一資料庫的一張表被拆分為兩張以上,EF似乎沒法映射了,兩張表欄位完全一致,難道再寫個Model?而且分表是個動態過程,也許一年分一次,也許一個月分一次,而且可能是定時任務去執行的,總不能一分表就改代碼。自此,我們和EF的矛盾激化了。
園子裡有關於資料庫拆分的部落格,我們所要改的只有資料訪問層,最好不要動業務層。而且我們上文也提到:用原生SQL。那麼我們就用原生SQL重寫資料訪問。
這裡舉個例子,比如我們拆分訂單表,按年拆分,通常使用者只會查看當年的訂單,所以主表的查詢次數肯定比其他分出的表要多,如果有使用者要查往年的訂單,我們再將查詢範圍擴大。按照這樣的理念,我們開始重寫資料訪問層,並按照以下要點執行:
1.使用原生SQL
2.添加日期參數,如果日期超出主表的範圍,則開始串連查詢
3.查詢中也可以添加其他條件,將參數作為對象填入SQL
4.抽象出一個產生SQL的公用方法,方便大家調用
拼裝SQL是一件極其考驗基本功的事,這個公用方法是我廠一位大師級的資料專家抽象出來的,大家也可以按照這個思路嘗試下,一個簡化版是很容易做出來的。
至此,項目中重要的業務功能已經和EF脫離關係了,我們也欣喜地收穫了SQL帶來的效率。而其他功能模組中,沒有高並發情境或並不常用的應用,我們並沒有做重構。
儘管本文標題是嫌棄,但如果是企業級應用,需要兼顧開發效率,且沒有互連網模式下的業務量激增狀況,筆者仍然推薦使用EF。
C#大型電商項目最佳化(二)——嫌棄EF與拋棄EF