性能
随着您应用程序的扩展,您需要确保它在负载和使用量增加的情况下运行良好。本文档提供了如何优化应用程序性能的指导。虽然您可以使用Parse Server进行快速原型设计,而不用担心性能,但在最初设计应用程序时,您还是需要考虑性能指标。我们强烈建议您在发布应用程序之前遵循所有建议。
您可以通过查看以下内容来改善应用程序的性能:
- 编写高效的查询。
- 写限制性查询。
- 使用客户端缓存。
- 使用Cloud Code。
- 避免计数查询。
- 使用有效的搜索技术。
请记住,并不是所有的建议都适用于您的应用程序。以下让我们逐一研究他们的细节。
1.编写高效查询
Parse对象存储在数据库中。Parse查询根据查询条件检索您感兴趣的对象。为了避免每个查询都翻遍某个Parse类中的所有数据,可在数据库中使用索引。索引是符合给定条件的有序列表。索引可帮助数据库进行高效的搜索并返回匹配结果,而不用查看所有数据。索引通常较小且在内存中,因此查找更快。
2.索引
使用Parse Server时,需要自行负责管理数据库和维护索引。如果您的数据未建立索引,则每个查询都必须遍历类中的所有数据以返回查询结果。另一方面,如果您的数据建立索引得当,为返回正确的查询结构,需扫描的文档数量将很少。
一些有用的查询约束命令如下:
- 等于(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对象的查询:
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字段创建索引查询将会比在playerName字段上创建索引的搜索空间更小。
考虑数据类型,布尔值的熵很低,因此它不会是好的索引。考虑以下查询约束:
query.equalTo("cheatMode", false);
对于"cheatMode",两个可能的值是true和false。如果在此字段上添加索引将没什么用处,因为很可能还是需要查看50%的记录才能返回查询结果。
数据类型按其键值值域的预期熵进行排名:
- GeoPoints
- Array
- Pointer
- Date
- String
- Number
- Other
即使最好的索引策略也比不上次优的查询。
3.高效查询的设计
编写高效的查询可充分利用索引的优势。我们来看看一些使得索引无效的查询约束:
- 不等于(Not Equal To)
- 不包含(Not Contained In)
此外,在某些情况下,如果它们无法利用索引,以下查询可能响应缓慢:
- 正则表达式
- 排序(Ordered By)
1.不等于(NOT EQUAL TO)
例如,假设您在GameScore类中查找游戏的高分。现在你想要检索除了某个玩家外的所有玩家的分数。您可以创建此查询:
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
});
此查询无法利用索引。数据库必须检查GameScore类中的所有对象以满足约束并检索结果。随着类中条目数量的增长,查询运行时间更长。
幸运的是,大多数时候,“Not Equal To”查询条件可以重写为“Contained In”条件。您需要检索匹配其他列值的值,而不是去查询缺失值。这样做数据库可使用索引,查询将更快。
例如,如果User类有一个名为state的列,它具有值“SignedUp”、“Verified”和“Invited”,那么查找所有至少使用过该应用程序一次的用户的缓慢方式是:
var query = new Parse.Query(Parse.User);
query.notEqualTo("state", "Invited");
而配置查询时使用“Contained In”条件会更快:
query.containedIn("state", ["SignedUp", "Verified"]);
有时,您可能需要完全重写您的查询。回到这个"GameScore"例子,假设我们正在查询并显示得分高于给定玩家的玩家。我们可以这样做,首先得到给定玩家的高分,然后使用以下查询语句:
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
});
您使用的新查询取决于您的用例。有时这可能意味着要重新设计您的数据模型。
2.不包含(NOT CONTAINED IN)
类似于“Not Equal To”,“Not Contained In”查询约束不能使用索引。您应该尝试使用互补的“Contained In”约束。基于之前的User示例,如果state列还有一个值“Blocked”,以表示被阻止的用户,则查找活动用户的慢查询是:
var query = new Parse.Query(Parse.User);
query.notContainedIn("state", ["Invited", "Blocked"]);
而使用互补的“Contained In”查询约束将始终更快:
query.containedIn("state", ["SignedUp", "Verified"]);
这意味着要相应地重写你的查询。而查询重写取决于您的Schema设置。这意味着可能要重做该Schema模式。
3.正则表达式
由于性能考虑,应避免使用正则表达式查询。MongoDB对于局部字符串匹配效率不高,除了仅需要前缀匹配的特殊情况。因此,包含正则表达式限制的查询代价高昂,特别是对于超过100,000条记录的类。在特定应用程序上,应该限制特定时间内运行这样操作的次数。
您应该避免运行不能使用索引的正则表达式约束。例如,以下查询在"playerName"字段中查找具有给定字符串的数据。字符串搜索不区分大小写,因此无法编入索引:
query.matches("playerName", "Michael", "i");
以下查询在大小写敏感的情况下,会查找字段中的任何字符串,并且无法编入索引:
query.contains("playerName", "Michael");
这些查询都很慢。事实上,我们的查询指南中故意没有包含matches和contains查询限制方法,因为我们不推荐使用它们。根据您的用例,您应切换到以下可使用索引的约束,例如:
query.startsWith("playerName", "Michael");
这将查找以给定字符串开头的数据。此查询将使用后端索引,因此即使对于大型数据集也将更快。
作为最佳实践,当您使用正则表达式约束时,您需要确保查询中的其他约束将结果集减少到数百个对象的有序列表,以使查询高效。如果由于遗留原因,您必须使用matches或contains约束,请尽可能使用区分大小写的固定查询,例如:
query.matches("playerName", "^Michael");
大多数使用正则表达式的用例涉及到执行搜索。执行搜索更有效的方法将在后面详细介绍。
4.写限制性查询
写限制性查询使得您仅返回客户端需要的数据。这在移动环境中至关重要,数据使用可能受到限制,而且网络连接不可靠。您还希望您的移动应用显示响应结果,这直接受发送回客户端对象的影响。“查询”章节介绍了可以添加到现有查询中以限制返回数据的约束类型。添加约束时,您需要注意设计高效的查询。
您可以使用skip和limit来浏览结果,并根据需要加载数据。缺省情况下,查询限制为100:
query.limit(10); // limit to at most 10 results
如果您在GeoPoints上发出查询,请确保指定合理的半径:
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
});
您可以进一步限制通过调用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.客户端缓存
对于从iOS和Android运行的查询,您可以打开查询缓存。有关详细信息,请参阅iOS和Android指南。缓存查询将提高您移动应用程序的性能,特别是在您要从Parse获取最新数据时显示缓存数据的情况下。
6.使用Cloud Code
Cloud Code允许您在Parse Server上运行自定义JavaScript逻辑,而不是在客户端上运行。
您可以利用它把处理分流到Parse服务器,从而增加应用程序端的感知性能。您可以创建保存或删除对象时运行的钩子(hooks)。这对于验证或清理数据很有用。您也可以使用Cloud Code修改相关对象或启动其他进程,例如发送推送通知。
我们看到了约束查询返回有限数据的例子。您还可以使用Cloud Functions来减少返回到应用程序的数据量。在以下示例中,我们使用Cloud Functions获取电影的平均评分:
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类中运行查询,仅返回stars字段数据并在客户端上计算结果。随着电影评论数量的增加,使用此方法返回到设备的数据也会增加。通过Cloud Functions实现该功能,如果成功,则只返回一个结果。
查看优化查询时,您会发现可能需要更改查询 —— 有时即使将应用程序发送到App Store或Google Play也是如此。如果您使用Cloud Functions,则无需客户端更新就可以更改查询。即使您必须重新设计Schema,您依然可以在保持客户端接口不变的情况下在Cloud Functions中更改,以避免应用程序更新。从之前平均标星数(average stars)的Cloud Functions示例中,从客户端SDK调用它如下所示:
Parse.Cloud.run("averageStars", { "movie": "The Matrix" }).then(function(ratings) {
// ratings is 4.5
});
如果以后,您需要修改基础数据模型,只要您返回一个表示评级结果的数字,您的客户端调用就可以保持不变。
7.避免计数操作
当需要频繁对对象计数时,应考虑在数据库中保存一个计数变量,每当添加对象时计数变量就递增。然后,可以简单地通过检索存储的计数变量来快速获得计数。
假设您在应用程序中显示电影信息,数据模型由一个Movie类和一个包含指向相应movie指针的Review类组成。您可能希望使用以下查询在顶级导航屏幕上显示每部电影的评论数:
var Review = Parse.Object.extend("Review");
var query = new Parse.Query("Review");
query.equalTo(“movie”, movie);
query.count().then(function(count) {
// Request succeeded
});
如果您为每个UI元素运行计数查询,则它们将无法在大型数据集上高效运行。避免使用count()操作符的一种方法是在Movie类中添加一个字段,该字段表示该电影的评论数。在保存Review类的条目时,需要在相应影片的评论计数字段加一。这可以在一个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;
});
});
新优化的查询使得您不需要查看Review类就可以获取评论数:
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
});
您还可以使用单独的Parse对象来跟踪每次评论的计数。每当添加或删除评论时,您可以在afterSave或afterDelete Cloud Code处理程序中增加或减少一个计数。您的方法取决于您的用例。
8.实施高效的搜索
如前所述,MongoDB对于局部字符串匹配效率不高。但是,在产品中实现一个扩展性很好的搜索功能,这是一个重要的用例。
简单的搜索算法直接扫描类中的所有数据,并对每个条目执行查询。使搜索高效运行的关键,是通过前述的索引来最小化每个查询执行时必须检查的数据量。您需要为要搜索的数据设计一个容易构建索引的数据模型。例如,不匹配确切前缀的字符串匹配查询由于无法使用索引,随着数据集增长将导致超时错误。
我们来看一个如何构建高效搜索的例子。您可以将此示例中的理念应用于自己的用例。比如你的应用程序有用户发帖,你想通过主题标签或特定的关键字搜索这些帖子。您将需要预处理帖子,并将主题标签(hashtags)和内容(words)列表保存到数组中。您可以在应用程序中保存帖子之前执行此处理,也可以使用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();
});
这可以将您的内容和主题标签保存在数组中,MongoDB将使用多键索引存储。这里要注意以下重要问题:首先,将所有单词转换为小写,以便我们可以使用小写查询查找它们,并获取不区分大小写的匹配。其次,它会滤除很多帖子中出现的常见词,如'the','in'和'and',以便在执行查询时进一步减少对索引的无用扫描。
一旦设置了关键字,您可以在查询中使用“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.限制和其他注意事项
- 这里有一些限制,以确保API可以高效提供您需要的数据。将来我们可能会调整这些限制。请花点时间阅读以下内容:
对象
- Parse对象的大小限制为128 KB。
- 我们建议不要在单个Parse对象上创建超过64个字段,以确保可以为查询构建高效的索引。
- 我们建议不要使用超过1,024个字符的字段名称,否则不会创建该字段的索引。
查询
- 默认情况下,查询返回100个对象。使用limit参数来改变它。
- skip和limit只能用于外部查询。
- 彼此冲突的约束将导致仅一个约束被执行。例如:在equalTo约束条件上,为同一个键名设置了两个不同的值,这两个值是矛盾的(也许你正在寻找'contains')。
- 地理查询中没有复合OR查询。
- 不建议使用$exists: false。
- JavaScript SDK中的each查询方法不能与地理位置约束的查询结合使用。
- 一个containsAll查询约束的比较数组中顶多只能包含9个项。
推送通知
- 通知的交付是“尽力而为”,不能保证。它不是要将数据传送到您的应用程序,只能通知用户有新的数据可用。
Cloud Code
- 传递到Cloud Code的参数有效载荷被限制为50 MB。