如何使用桶模式进行分页——第二讲

#数据模型

在使用桶模式进行分页的上一讲,我们探讨了强大的桶模式以及利用它实现简单、高效分页的方法。本文将更深入地探讨分页功能,并有针对性地介绍创建桶的简单方法。

如何通过一个数组中的多条交易,使用一个单独的文档,快速生成股票交易清单。如果每个网页显示20条交易,那么,它在50个文档中就能存储1,000条交易。操作时,简单地提取与所需显示页面相对应的文档即可。如希望显示其中的第21条交易,则提取第2个文档即可。

有心的读者会注意到,skip又一次出现了。我们已经探讨过使用skip的缺点,但请回忆一下,与跳过文档操作相关的分页缓慢这种情况,只在服务器必须查找大量文档以及返回所需限量文档时才会出现。使用桶模式明显减少了归属于单一客户的文档数量。当每个文档包含多条交易,文档数量就会明显减少。对比于每个文档只包含一条交易的方式,此时执行跳过操作就不会面临相同的性能问题。

但如果用户希望每页能查看100条交易,会出现什么情况呢?每个桶只存储20个对象,这个数量确实很有限。解决方案也比较简单。作为代替,可以存储10个桶,每个桶包含100个对象。没人说过每个桶不能包含比必需数量更多的对象!实际上,1,000个对象也可以存储在一个桶里。那样的话,如果每页显示25条交易,同一个桶可以使用40次;如果每页显示50条交易,同一个桶可以使用20次;如果每页显示100条交易,同一个桶则可以使用10次,以此类推。

按此逻辑,接下来的问题便是:“为什么不将所有信息存储在一个文档中?”有几个原因。第一,一个单独的大容量文件需要消耗很多内存。服务器必须分配一些内存用来提取文档,客户端也必须分配一些内存用于接收文档。第二,大文档在网络上传输会比较慢。第三,MongoDB将文档大小的最大值限定为16MB(特别设定)。选择一个合理的文档大小并坚持使用下去。所用的桶容量为100、500或1,000。更大容量的桶(如10,000的桶)在一些对象较小的应用场合可能比较适宜,容量再大可能就有些过了。

好了!我们找到了桶的合理大小,它能存储1,000条交易,从任何页面都能迅速、高效地显示历史交易信息。

那么,如何将数据插入这些桶中?所需的工作量比你想象的要少得多。通过一个例子就很容易理解了。

假定发生了一笔新的股票购买交易,我们希望记录这次交易的历史信息。以下是运行的插入语句:

在这个语句中,有很多信息需要拆包,因此,我们要一步一步去完成它。

我们的每个桶都包含在历史采集信息中。在history采集过程中,我们使用了updateOne语句。需要强调一下,我们使用的是update语句,不是insert语句,而且,我们只更新一个文档。因为对于customerId 7000000的客户来说,可能已经存在一个或多个桶了。我们希望将一条新交易插入到一个单独的现有桶中。稍后,我们将继续讨论在没有桶存在的情况下会发生什么。

Count字段确保了单独的桶不会超过1,000条交易。第一个被找到的内含对象数量小于或等于999的桶将被更新。包含1,000或1,000个以上交易的桶将被忽略,这是由于它们不满足查询条件。

$push运算符为数组的末尾添加了一个元素。交易历史信息通过history字段表示,因此,在该字段增补一条新交易是应该采取的正确步骤。

必须对count字段进行原子维护,以体现准确的交易数量。通过使用$inc运算符,对于每一条新插入的交易,桶的count字段也会相应加1。

最后,配合使用upsert修饰符和$setOnInsert运算符,解决“没有现存桶”的情况。

在两种条件下,查询条件会阻止文档被更新:对于特定的customerId(_id的第一部分)来说,尚无相应的桶存在;以及在现有桶中,没有桶包含的交易数小于1,000。如果查询条件无法更新现有文档,可使用upsert创建一个新文档。换句话说,如果没有现有的桶可以更新,通过updateOne 语句可以创建一个新的桶。

当创建一个新的桶之后,通过$setOnInsert运算符可以设置_id。请回忆一下,_id是一个复合/串接字符串,它包含customerId和桶中第一次交易的时间。该应用构建了这个字段,并作为updateOne操作的一部分。首先,customerId是已知的,因为它是搜索条件的一部分。其次,与新桶的第一次交易时间相关的交易一般就是被插入的交易。因此,由于两个字段均为已知的,构建_id就变得很简单。提取customerId以及当前交易日期,将其串接为$setOnInsert值。如果不需要创建新的桶,则无需关注上述运算符(和字符串)。

最后的文档看起来如同上面用于显示的文档一样。瞧!使用一个单独的文档和一条更新语句就完成了分页。

不过,这种方式也存在一定的局限性。例如,如果你需要从一个桶中移除一条交易,会发生什么?会产生一个间隙,可能导致文档的下一条updateOne语句更新错误。MongoDB 4.2版通过表意式的更新说明(关于这个问题,可参见最新的开始进入MongoDB 4.2的文章)避免了这种限制。这种方法还规定了排列顺序。新的交易通常会增补到排列顺序的末尾(本案例中是时间顺序)。对不同字段进行排序需要采取聚合的方式,使用$unwind操作符。虽然存在这些局限性,但如果能有效利用桶模式,就能应对很多的应用场景。

我们在这两篇文章中探讨了两个选项。就像所有的工程挑战一样,每个选项都存在着自身的优、缺点。我们探讨了如何对查找语句的查询部分作出修改,以对文档执行“跳过”操作;此外,我们还探讨了使用桶模式对结果进行预分页。使用哪种方法效果最好取决于特定的应用场景。

请记住,最好能限定用户界面的功能,以获得更好的用户体验。根据用户需要选择一种方法,进行合理设计。有很多方法可选择,每种方法都能获得卓越的性能,并能快捷地发布出去!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190819A03HZA00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券