性能
隨著您應(yīng)用程序的擴(kuò)展,您需要確保它在負(fù)載和使用量增加的情況下運(yùn)行良好声离。本文檔提供了如何優(yōu)化應(yīng)用程序性能的指導(dǎo)宾娜。雖然您可以使用Parse Server進(jìn)行快速原型設(shè)計(jì),而不用擔(dān)心性能吩谦,但在最初設(shè)計(jì)應(yīng)用程序時(shí)鸳谜,您還是需要考慮性能指標(biāo)。我們強(qiáng)烈建議您在發(fā)布應(yīng)用程序之前遵循所有建議式廷。
您可以通過查看以下內(nèi)容來改善應(yīng)用程序的性能:
- 編寫高效的查詢咐扭。
- 寫限制性查詢。
- 使用客戶端緩存。
- 使用Cloud Code草描。
- 避免計(jì)數(shù)查詢览绿。
- 使用有效的搜索技術(shù)。
請(qǐng)記住穗慕,并不是所有的建議都適用于您的應(yīng)用程序饿敲。以下讓我們逐一研究他們的細(xì)節(jié)。
1.編寫高效查詢
Parse對(duì)象存儲(chǔ)在數(shù)據(jù)庫中逛绵。Parse查詢根據(jù)查詢條件檢索您感興趣的對(duì)象怀各。為了避免每個(gè)查詢都翻遍某個(gè)Parse類中的所有數(shù)據(jù),可在數(shù)據(jù)庫中使用索引术浪。索引是符合給定條件的有序列表瓢对。索引可幫助數(shù)據(jù)庫進(jìn)行高效的搜索并返回匹配結(jié)果,而不用查看所有數(shù)據(jù)胰苏。索引通常較小且在內(nèi)存中硕蛹,因此查找更快。
2.索引
使用Parse Server時(shí)硕并,需要自行負(fù)責(zé)管理數(shù)據(jù)庫和維護(hù)索引法焰。如果您的數(shù)據(jù)未建立索引,則每個(gè)查詢都必須遍歷類中的所有數(shù)據(jù)以返回查詢結(jié)果倔毙。另一方面埃仪,如果您的數(shù)據(jù)建立索引得當(dāng),為返回正確的查詢結(jié)構(gòu)陕赃,需掃描的文檔數(shù)量將很少卵蛉。
一些有用的查詢約束命令如下:
- 等于(Equal to)
- 包含在(Contained In)
- 小于,小于或等于么库,大于傻丝,大于或等于(Less than, Less than or Equal to, Greater than, Greater than or Equal to)
- 前綴字符串匹配(Prefix string matches)
- 不等于(Not equal to)
- 不包含在(Not contained in)
- 其他一切(Everything else)
我們來看一下以下檢索GameScore對(duì)象的查詢:
var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
query.equalTo("score", 50);
query.containedIn("playerName",
["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);
在score字段創(chuàng)建索引查詢將會(huì)比在playerName字段上創(chuàng)建索引的搜索空間更小。
考慮數(shù)據(jù)類型廊散,布爾值的熵很低桑滩,因此它不會(huì)是好的索引≡识茫考慮以下查詢約束:
query.equalTo("cheatMode", false);
對(duì)于"cheatMode"运准,兩個(gè)可能的值是true和false。如果在此字段上添加索引將沒什么用處缭受,因?yàn)楹芸赡苓€是需要查看50%的記錄才能返回查詢結(jié)果胁澳。
數(shù)據(jù)類型按其鍵值值域的預(yù)期熵進(jìn)行排名:
- GeoPoints
- Array
- Pointer
- Date
- String
- Number
- Other
即使最好的索引策略也比不上次優(yōu)的查詢。
3.高效查詢的設(shè)計(jì)
編寫高效的查詢可充分利用索引的優(yōu)勢(shì)米者。我們來看看一些使得索引無效的查詢約束:
- 不等于(Not Equal To)
- 不包含(Not Contained In)
此外韭畸,在某些情況下宇智,如果它們無法利用索引,以下查詢可能響應(yīng)緩慢:
- 正則表達(dá)式
- 排序(Ordered By)
1.不等于(NOT EQUAL TO)
例如胰丁,假設(shè)您在GameScore類中查找游戲的高分∷骈伲現(xiàn)在你想要檢索除了某個(gè)玩家外的所有玩家的分?jǐn)?shù)。您可以創(chuàng)建此查詢:
var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
query.notEqualTo("playerName", "Michael Yabuti");
query.find().then(function(results) {
// Retrieved scores successfully
});
此查詢無法利用索引锦庸。數(shù)據(jù)庫必須檢查GameScore類中的所有對(duì)象以滿足約束并檢索結(jié)果机蔗。隨著類中條目數(shù)量的增長,查詢運(yùn)行時(shí)間更長甘萧。
幸運(yùn)的是萝嘁,大多數(shù)時(shí)候,“Not Equal To”查詢條件可以重寫為“Contained In”條件扬卷。您需要檢索匹配其他列值的值牙言,而不是去查詢?nèi)笔е怠_@樣做數(shù)據(jù)庫可使用索引怪得,查詢將更快咱枉。
例如,如果User類有一個(gè)名為state的列,它具有值“SignedUp”、“Verified”和“Invited”垢揩,那么查找所有至少使用過該應(yīng)用程序一次的用戶的緩慢方式是:
var query = new Parse.Query(Parse.User);
query.notEqualTo("state", "Invited");
而配置查詢時(shí)使用“Contained In”條件會(huì)更快:
query.containedIn("state", ["SignedUp", "Verified"]);
有時(shí)闹获,您可能需要完全重寫您的查詢⊙瞻茫回到這個(gè)"GameScore"例子财岔,假設(shè)我們正在查詢并顯示得分高于給定玩家的玩家。我們可以這樣做河爹,首先得到給定玩家的高分匠璧,然后使用以下查詢語句:
var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
// Previously retrieved highScore for Michael Yabuti
query.greaterThan("score", highScore);
query.find().then(function(results) {
// Retrieved scores successfully
});
您使用的新查詢?nèi)Q于您的用例。有時(shí)這可能意味著要重新設(shè)計(jì)您的數(shù)據(jù)模型咸这。
2.不包含(NOT CONTAINED IN)
類似于“Not Equal To”夷恍,“Not Contained In”查詢約束不能使用索引。您應(yīng)該嘗試使用互補(bǔ)的“Contained In”約束媳维∧鹧基于之前的User示例,如果state列還有一個(gè)值“Blocked”侄刽,以表示被阻止的用戶指黎,則查找活動(dòng)用戶的慢查詢是:
var query = new Parse.Query(Parse.User);
query.notContainedIn("state", ["Invited", "Blocked"]);
而使用互補(bǔ)的“Contained In”查詢約束將始終更快:
query.containedIn("state", ["SignedUp", "Verified"]);
這意味著要相應(yīng)地重寫你的查詢。而查詢重寫取決于您的Schema設(shè)置州丹。這意味著可能要重做該Schema模式醋安。
3.正則表達(dá)式
由于性能考慮杂彭,應(yīng)避免使用正則表達(dá)式查詢。MongoDB對(duì)于局部字符串匹配效率不高吓揪,除了僅需要前綴匹配的特殊情況亲怠。因此,包含正則表達(dá)式限制的查詢代價(jià)高昂柠辞,特別是對(duì)于超過100,000條記錄的類团秽。在特定應(yīng)用程序上,應(yīng)該限制特定時(shí)間內(nèi)運(yùn)行這樣操作的次數(shù)钾腺。
您應(yīng)該避免運(yùn)行不能使用索引的正則表達(dá)式約束徙垫。例如,以下查詢?cè)?playerName"字段中查找具有給定字符串的數(shù)據(jù)放棒。字符串搜索不區(qū)分大小寫姻报,因此無法編入索引:
query.matches("playerName", "Michael", "i");
以下查詢?cè)诖笮懨舾械那闆r下,會(huì)查找字段中的任何字符串间螟,并且無法編入索引:
query.contains("playerName", "Michael");
這些查詢都很慢吴旋。事實(shí)上,我們的查詢指南中故意沒有包含matches和contains查詢限制方法厢破,因?yàn)槲覀儾煌扑]使用它們荣瑟。根據(jù)您的用例,您應(yīng)切換到以下可使用索引的約束摩泪,例如:
query.startsWith("playerName", "Michael");
這將查找以給定字符串開頭的數(shù)據(jù)笆焰。此查詢將使用后端索引,因此即使對(duì)于大型數(shù)據(jù)集也將更快见坑。
作為最佳實(shí)踐嚷掠,當(dāng)您使用正則表達(dá)式約束時(shí),您需要確保查詢中的其他約束將結(jié)果集減少到數(shù)百個(gè)對(duì)象的有序列表荞驴,以使查詢高效不皆。如果由于遺留原因,您必須使用matches或contains約束熊楼,請(qǐng)盡可能使用區(qū)分大小寫的固定查詢霹娄,例如:
query.matches("playerName", "^Michael");
大多數(shù)使用正則表達(dá)式的用例涉及到執(zhí)行搜索。執(zhí)行搜索更有效的方法將在后面詳細(xì)介紹鲫骗。
4.寫限制性查詢
寫限制性查詢使得您僅返回客戶端需要的數(shù)據(jù)犬耻。這在移動(dòng)環(huán)境中至關(guān)重要,數(shù)據(jù)使用可能受到限制挎峦,而且網(wǎng)絡(luò)連接不可靠香追。您還希望您的移動(dòng)應(yīng)用顯示響應(yīng)結(jié)果,這直接受發(fā)送回客戶端對(duì)象的影響坦胶⊥傅洌“查詢”章節(jié)介紹了可以添加到現(xiàn)有查詢中以限制返回?cái)?shù)據(jù)的約束類型晴楔。添加約束時(shí),您需要注意設(shè)計(jì)高效的查詢峭咒。
您可以使用skip和limit來瀏覽結(jié)果税弃,并根據(jù)需要加載數(shù)據(jù)。缺省情況下凑队,查詢限制為100:
query.limit(10); // limit to at most 10 results
如果您在GeoPoints上發(fā)出查詢则果,請(qǐng)確保指定合理的半徑:
var query = new Parse.Query(PlaceObject);
query.withinMiles("location", userGeoPoint, 10.0);
query.find().then(function(placesObjects) {
// Get a list of objects within 10 miles of a user's location
});
您可以進(jìn)一步限制通過調(diào)用select返回的字段:
var GameScore = Parse.Object.extend("GameScore");
var query = new Parse.Query(GameScore);
query.select("score", "playerName");
query.find().then(function(results) {
// each of results will only have the selected fields available.
});
5.客戶端緩存
對(duì)于從iOS和Android運(yùn)行的查詢,您可以打開查詢緩存漩氨。有關(guān)詳細(xì)信息西壮,請(qǐng)參閱iOS和Android指南。緩存查詢將提高您移動(dòng)應(yīng)用程序的性能叫惊,特別是在您要從Parse獲取最新數(shù)據(jù)時(shí)顯示緩存數(shù)據(jù)的情況下款青。
6.使用Cloud Code
Cloud Code允許您在Parse Server上運(yùn)行自定義JavaScript邏輯,而不是在客戶端上運(yùn)行霍狰。
您可以利用它把處理分流到Parse服務(wù)器抡草,從而增加應(yīng)用程序端的感知性能。您可以創(chuàng)建保存或刪除對(duì)象時(shí)運(yùn)行的鉤子(hooks)蔗坯。這對(duì)于驗(yàn)證或清理數(shù)據(jù)很有用康震。您也可以使用Cloud Code修改相關(guān)對(duì)象或啟動(dòng)其他進(jìn)程,例如發(fā)送推送通知宾濒。
我們看到了約束查詢返回有限數(shù)據(jù)的例子腿短。您還可以使用Cloud Functions來減少返回到應(yīng)用程序的數(shù)據(jù)量。在以下示例中绘梦,我們使用Cloud Functions獲取電影的平均評(píng)分:
Parse.Cloud.define("averageStars", function(request, response) {
var Review = Parse.Object.extend("Review");
var query = new Parse.Query(Review);
query.equalTo("movie", request.params.movie);
query.find().then(function(results) {
var sum = 0;
for (var i = 0; i < results.length; ++i) {
sum += results[i].get("stars");
}
response.success(sum / results.length);
}, function(error) {
response.error("movie lookup failed");
});
});
您可以在客戶端上的Review類中運(yùn)行查詢答姥,僅返回stars字段數(shù)據(jù)并在客戶端上計(jì)算結(jié)果。隨著電影評(píng)論數(shù)量的增加谚咬,使用此方法返回到設(shè)備的數(shù)據(jù)也會(huì)增加。通過Cloud Functions實(shí)現(xiàn)該功能尚粘,如果成功择卦,則只返回一個(gè)結(jié)果。
查看優(yōu)化查詢時(shí)郎嫁,您會(huì)發(fā)現(xiàn)可能需要更改查詢 —— 有時(shí)即使將應(yīng)用程序發(fā)送到App Store或Google Play也是如此秉继。如果您使用Cloud Functions,則無需客戶端更新就可以更改查詢泽铛。即使您必須重新設(shè)計(jì)Schema尚辑,您依然可以在保持客戶端接口不變的情況下在Cloud Functions中更改,以避免應(yīng)用程序更新盔腔。從之前平均標(biāo)星數(shù)(average stars)的Cloud Functions示例中杠茬,從客戶端SDK調(diào)用它如下所示:
Parse.Cloud.run("averageStars", { "movie": "The Matrix" }).then(function(ratings) {
// ratings is 4.5
});
如果以后月褥,您需要修改基礎(chǔ)數(shù)據(jù)模型,只要您返回一個(gè)表示評(píng)級(jí)結(jié)果的數(shù)字瓢喉,您的客戶端調(diào)用就可以保持不變宁赤。
7.避免計(jì)數(shù)操作
當(dāng)需要頻繁對(duì)對(duì)象計(jì)數(shù)時(shí),應(yīng)考慮在數(shù)據(jù)庫中保存一個(gè)計(jì)數(shù)變量栓票,每當(dāng)添加對(duì)象時(shí)計(jì)數(shù)變量就遞增决左。然后,可以簡單地通過檢索存儲(chǔ)的計(jì)數(shù)變量來快速獲得計(jì)數(shù)走贪。
假設(shè)您在應(yīng)用程序中顯示電影信息佛猛,數(shù)據(jù)模型由一個(gè)Movie類和一個(gè)包含指向相應(yīng)movie指針的Review類組成。您可能希望使用以下查詢?cè)陧敿?jí)導(dǎo)航屏幕上顯示每部電影的評(píng)論數(shù):
var Review = Parse.Object.extend("Review");
var query = new Parse.Query("Review");
query.equalTo(“movie”, movie);
query.count().then(function(count) {
// Request succeeded
});
如果您為每個(gè)UI元素運(yùn)行計(jì)數(shù)查詢坠狡,則它們將無法在大型數(shù)據(jù)集上高效運(yùn)行继找。避免使用count()操作符的一種方法是在Movie類中添加一個(gè)字段,該字段表示該電影的評(píng)論數(shù)擦秽。在保存Review類的條目時(shí)码荔,需要在相應(yīng)影片的評(píng)論計(jì)數(shù)字段加一。這可以在一個(gè)afterSave處理方法中完成:
Parse.Cloud.afterSave("Review", function(request) {
// Get the movie id for the Review
var movieId = request.object.get("movie").id;
// Query the Movie represented by this review
var Movie = Parse.Object.extend("Movie");
var query = new Parse.Query(Movie);
query.get(movieId).then(function(movie) {
// Increment the reviews field on the Movie object
movie.increment("reviews");
movie.save();
}, function(error) {
throw "Got an error " + error.code + " : " + error.message;
});
});
新優(yōu)化的查詢使得您不需要查看Review類就可以獲取評(píng)論數(shù):
var Movie = Parse.Object.extend("Movie");
var query = new Parse.Query(Movie);
query.find().then(function(results) {
// Results include the reviews count field
}, function(error) {
// Request failed
});
您還可以使用單獨(dú)的Parse對(duì)象來跟蹤每次評(píng)論的計(jì)數(shù)感挥。每當(dāng)添加或刪除評(píng)論時(shí)缩搅,您可以在afterSave或afterDelete Cloud Code處理程序中增加或減少一個(gè)計(jì)數(shù)。您的方法取決于您的用例触幼。
8.實(shí)施高效的搜索
如前所述硼瓣,MongoDB對(duì)于局部字符串匹配效率不高。但是置谦,在產(chǎn)品中實(shí)現(xiàn)一個(gè)擴(kuò)展性很好的搜索功能堂鲤,這是一個(gè)重要的用例。
簡單的搜索算法直接掃描類中的所有數(shù)據(jù)媒峡,并對(duì)每個(gè)條目執(zhí)行查詢瘟栖。使搜索高效運(yùn)行的關(guān)鍵,是通過前述的索引來最小化每個(gè)查詢執(zhí)行時(shí)必須檢查的數(shù)據(jù)量谅阿。您需要為要搜索的數(shù)據(jù)設(shè)計(jì)一個(gè)容易構(gòu)建索引的數(shù)據(jù)模型半哟。例如,不匹配確切前綴的字符串匹配查詢由于無法使用索引签餐,隨著數(shù)據(jù)集增長將導(dǎo)致超時(shí)錯(cuò)誤寓涨。
我們來看一個(gè)如何構(gòu)建高效搜索的例子。您可以將此示例中的理念應(yīng)用于自己的用例氯檐。比如你的應(yīng)用程序有用戶發(fā)帖戒良,你想通過主題標(biāo)簽或特定的關(guān)鍵字搜索這些帖子。您將需要預(yù)處理帖子冠摄,并將主題標(biāo)簽(hashtags)和內(nèi)容(words)列表保存到數(shù)組中糯崎。您可以在應(yīng)用程序中保存帖子之前執(zhí)行此處理几缭,也可以使用Cloud Code的beforeSave鉤子:
var _ = require("underscore");
Parse.Cloud.beforeSave("Post", function(request, response) {
var post = request.object;
var toLowerCase = function(w) { return w.toLowerCase(); };
var words = post.get("text").split(/\b/);
words = _.map(words, toLowerCase);
var stopWords = ["the", "in", "and"]
words = _.filter(words, function(w) {
return w.match(/^\w+$/) && ! _.contains(stopWords, w);
});
var hashtags = post.get("text").match(/#.+?\b/g);
hashtags = _.map(hashtags, toLowerCase);
post.set("words", words);
post.set("hashtags", hashtags);
response.success();
});
這可以將您的內(nèi)容和主題標(biāo)簽保存在數(shù)組中,MongoDB將使用多鍵索引存儲(chǔ)拇颅。這里要注意以下重要問題:首先奏司,將所有單詞轉(zhuǎn)換為小寫,以便我們可以使用小寫查詢查找它們樟插,并獲取不區(qū)分大小寫的匹配韵洋。其次,它會(huì)濾除很多帖子中出現(xiàn)的常見詞黄锤,如'the'搪缨,'in'和'and',以便在執(zhí)行查詢時(shí)進(jìn)一步減少對(duì)索引的無用掃描鸵熟。
一旦設(shè)置了關(guān)鍵字副编,您可以在查詢中使用“All”約束來高效地檢索:
var Post = Parse.Object.extend("Post");
var query = new Parse.Query(Post);
query.containsAll("hashtags", [“#parse”, “#ftw”]);
query.find().then(function(results) {
// Request succeeded
}, function(error) {
// Request failed
});
9.限制和其他注意事項(xiàng)
- 這里有一些限制,以確保API可以高效提供您需要的數(shù)據(jù)流强。將來我們可能會(huì)調(diào)整這些限制痹届。請(qǐng)花點(diǎn)時(shí)間閱讀以下內(nèi)容:
對(duì)象
- Parse對(duì)象的大小限制為128 KB。
- 我們建議不要在單個(gè)Parse對(duì)象上創(chuàng)建超過64個(gè)字段打月,以確倍痈可以為查詢構(gòu)建高效的索引。
- 我們建議不要使用超過1,024個(gè)字符的字段名稱奏篙,否則不會(huì)創(chuàng)建該字段的索引柴淘。
查詢
- 默認(rèn)情況下,查詢返回100個(gè)對(duì)象秘通。使用limit參數(shù)來改變它为严。
- skip和limit只能用于外部查詢。
- 彼此沖突的約束將導(dǎo)致僅一個(gè)約束被執(zhí)行肺稀。例如:在equalTo約束條件上第股,為同一個(gè)鍵名設(shè)置了兩個(gè)不同的值,這兩個(gè)值是矛盾的(也許你正在尋找'contains')话原。
- 地理查詢中沒有復(fù)合OR查詢炸茧。
- 不建議使用$exists: false。
- JavaScript SDK中的each查詢方法不能與地理位置約束的查詢結(jié)合使用稿静。
- 一個(gè)containsAll查詢約束的比較數(shù)組中頂多只能包含9個(gè)項(xiàng)。
推送通知
- 通知的交付是“盡力而為”辕狰,不能保證改备。它不是要將數(shù)據(jù)傳送到您的應(yīng)用程序,只能通知用戶有新的數(shù)據(jù)可用蔓倍。
Cloud Code
- 傳遞到Cloud Code的參數(shù)有效載荷被限制為50 MB悬钳。