RavenDB Tips: 巧妙設(shè)計(jì)文檔的Id格式以提升查詢(xún)性能
1. RavenDB中的兩種查詢(xún)方式
在RavenDB中,查詢(xún)可分為兩種,一種是通過(guò)IDocumentSession的Load方法按Id直接查詢(xún),另一種是通過(guò)IDocumentSession的Query<T>方法查詢(xún)索引。如下所示:
// 使用Load方法查詢(xún)Id為users/1的User var user = session.Load<User>("users/1"); // 使用Query<T>方法查詢(xún)索引,索引名為UserIndex var users = session.Query<User>("UserIndex");
這兩種查詢(xún)的主要區(qū)別在于后者是查詢(xún)索引,而前者不是。RavenDB的索引是在后臺(tái)計(jì)算的,這也就意味著查詢(xún)索引得到的很可能是臟數(shù)據(jù),例如,我們添加一個(gè)用戶(hù)并成功保存,然后通過(guò)索引查詢(xún)用戶(hù),這時(shí)是有可能查詢(xún)不到剛剛保存的用戶(hù)記錄的。而對(duì)于Load方法,只要用戶(hù)保存成功了,調(diào)用Load<User>(userId)則一定會(huì)查詢(xún)到相應(yīng)的用戶(hù)記錄,且不為臟數(shù)據(jù)。
2. 業(yè)務(wù)流程處理中對(duì)強(qiáng)一致性的要求
顯然,RavenDB的高性能正是來(lái)源于索引查詢(xún)的這種“最終一致性”,因?yàn)閷?duì)于大部分應(yīng)用來(lái)說(shuō)(尤其互聯(lián)網(wǎng)應(yīng)用),幾乎都是讀多寫(xiě)少,而且可以接受臨時(shí)的數(shù)據(jù)不一致。但對(duì)于業(yè)務(wù)流程的處理來(lái)說(shuō),往往要求數(shù)據(jù)是強(qiáng)一致的,例如,某系統(tǒng)中有User和Account兩個(gè)類(lèi),前者表示用戶(hù),后者表示該用戶(hù)相關(guān)聯(lián)的帳號(hào)(Account中有Balance(表示帳戶(hù)余額)這樣的屬性),User和Account是一對(duì)一的關(guān)系,并且這兩個(gè)文檔獨(dú)立存儲(chǔ),也就是說(shuō)不把Account存儲(chǔ)為User文檔的內(nèi)嵌對(duì)象。現(xiàn)假設(shè)有一個(gè)用戶(hù)購(gòu)買(mǎi)商品的流程,購(gòu)買(mǎi)商品需要支付商品費(fèi)用,此時(shí)該用戶(hù)(User)的帳戶(hù)(Account)需要扣除相應(yīng)的金額,用偽代碼可表示為:
var user = ...; var account = GetAccount(user); account.Decrease(50); // 假設(shè)需要支付50元
上面代碼的問(wèn)題就在于GetAccount()方法要怎么寫(xiě),對(duì)于這個(gè)業(yè)務(wù)流程來(lái)講,調(diào)用GetAccount()時(shí)必須得到該用戶(hù)最新的帳戶(hù)信息,不能得到臟數(shù)據(jù),如果我們?cè)贕etAccount()中通過(guò)查詢(xún)索引來(lái)獲取Account實(shí)例,則可能查不到Account,或查到的Account的Balance是臟數(shù)據(jù),這是無(wú)法接受的。不過(guò),前面說(shuō)的其實(shí)并不完全正確,在RavenDB中查詢(xún)索引,是可以強(qiáng)制不返回臟數(shù)據(jù)的(即等待索引計(jì)算結(jié)束后再返回),用代碼來(lái)表示就是:
var user = ...; var account = session.Query<Account>("AccountIndex") .Where(x => x.UserId == user.Id) .Customize(x => x.WaitForNonStaleResultsAsOfNow()) .First();
上面代碼中的Customize(x => x.WaitForNonStaleResultsAsOfNow())會(huì)等待索引計(jì)算結(jié)束后再返回,這樣得到的Account就可以保證是最新的,但這里有一個(gè)大問(wèn)題:這個(gè)等待可能很快,也可能很慢。這也相當(dāng)于把兩個(gè)本來(lái)并行的操作改成同步執(zhí)行,性能殺手!對(duì)于一個(gè)設(shè)計(jì)良好的RavenDB應(yīng)用來(lái)說(shuō),WaitForNonStatleResultsAsOfNow()這個(gè)方法應(yīng)該基本不使用,如果我們發(fā)現(xiàn)代碼中大量調(diào)用了該方法,那我們的代碼一定是有問(wèn)題的(當(dāng)然,在單元測(cè)試中則可能會(huì)大量用到該方法)。現(xiàn)在問(wèn)題有了,就要找解決方案。
3. 強(qiáng)一致性和高性能兼得的方案
解決思路其實(shí)很明顯,如果我們可以通過(guò)Load來(lái)加載Account,那問(wèn)題便迎刃而解,而如果想通過(guò)Load來(lái)加載Account,那就需要一種通過(guò)User實(shí)例來(lái)計(jì)算Account的Id的辦法,因?yàn)閁ser是已知的,只要可以根據(jù)User中的信息來(lái)計(jì)算出Account的Id,那就可以通過(guò)Load來(lái)加載Account了,所以,解決方案就是:將Account的Id格式設(shè)計(jì)成UserId和/account的拼接。用代碼表示如下:
創(chuàng)建用戶(hù):
// 創(chuàng)建User var user = new User(); // ... session.Store(user); // 創(chuàng)建Account,注意Id的格式 var account = new Account(); account.Id = user.Id + "/account"; session.Store(account);
查詢(xún)Account:
var user = session.Load<User>("已知的UserId"); // 通過(guò)User計(jì)算Account的Id var accountId = user.Id + "/account"; // 通過(guò)計(jì)算出來(lái)的Account Id直接加載Account var account = session.Load<Account>(accountId);
這樣,我們的代碼就不再需要WaitForNonStatleResultsAsOfNow()了,而且,上面的代碼很容易進(jìn)行進(jìn)一步優(yōu)化,例如將User和Account放在一個(gè)數(shù)據(jù)庫(kù)請(qǐng)求中加載(Load方法中可以傳入多個(gè)不同實(shí)體的Id同時(shí)加載),這樣就可以減少一次數(shù)據(jù)庫(kù)連接的性能消耗。
4. 一對(duì)多關(guān)聯(lián)的處理
上面是一對(duì)一的情況,對(duì)于一對(duì)多的情況一樣適用(當(dāng)然這個(gè)“多”不能太多,一般就幾條為宜),假設(shè)一個(gè)User可以關(guān)聯(lián)多個(gè)Account(關(guān)聯(lián)的Account總數(shù)不多),這時(shí)可以將Account的Id格式設(shè)計(jì)成: UserId + /account/ + 數(shù)字,例如Id為users/1的用戶(hù)關(guān)聯(lián)的Account Id可以有users/1/account/1, users/1/account/2等,在這種情況下,加載指定用戶(hù)的Account可以利用IDocumentSession的Advanced.LoadStartingWith<T>()方法。
RavenDB服務(wù)端的Id默認(rèn)是users/11、products/23、orders/12這樣的格式,而客戶(hù)端則可通過(guò)約定來(lái)支持整型的Id,也就是說(shuō),我們可以在程序中將User的Id定義為Int32類(lèi)型(這只是約定,RavenDB的客戶(hù)端類(lèi)庫(kù)會(huì)實(shí)現(xiàn)客戶(hù)端Int32 Id到服務(wù)端的字符串Id的轉(zhuǎn)換),但通過(guò)上面的例子也可以看出,在RavenDB中更推薦直接使用字符串格式的Id,因?yàn)槲覀兛梢栽谧址甀d的格式上做很多文章。
另外,通過(guò)上面關(guān)于查詢(xún)的討論也可以看到,想通過(guò)網(wǎng)上廣傳的Repository模式來(lái)讓?xiě)?yīng)用可同時(shí)支持RDBMS和RavenDB的做法是不可行的,RDBMS的查詢(xún)可以返回強(qiáng)一致的結(jié)果,而RavenDB中的索引查詢(xún)則是最終一致的,若要讓Repository中的查詢(xún)接口返回強(qiáng)一致的結(jié)果,則要使用WaitForNonStatleResults(),而這會(huì)對(duì)性能產(chǎn)生很大影響。
浙公網(wǎng)安備 33010602011771號(hào)