电商参考架构第三部分:查询优化及扩展

在本系列电商参考架构的第一部分中,我们讨论了如何使用MongoDB作为一个大型产品目录的数据持久层。在第二部分中,我们介绍了库存系统的模式及数据模型。今天,我们将介绍如何查询和更新库存以及如何扩展系统。

库存更新及聚合

一个好的库存系统不应该只是一个检索静态数据的记录系统。我们还需要能够在库存上执行一些操作,包括当库存变化时对数据记录的修改以及为获得产品目前可用库存及库存地点完整视图而做的聚合操作。

其中的第一个操作——更新库存,是一个非常直接的并且和一个标准查询一样高效的,这就意味着我们的库存系统需要能够处理我们预计接收到的大数据量。为了使用MongoDB实现这个功能,我们简单地通过它的‘productId’来检索一个商品,然后使用$inc操作符在我们希望更新的产品系列上执行一个即时更新。

 db.inventory.update(
{
  “storeId”:”store100”,
  “productId”:“20034”,
  “vars.sku”:”sku11736”
},
  {“$inc”:{“vars.$.q”:20}}
)

针对于库存的聚合统计,MongoDB的聚合管道框架允许让我们获取一系列查询结果并且这些结果应用多阶段的数据转化功能。通过这种方式为我们提供了许多除了单店库存之外的更有价值的数据视图。例如,假设我们想要找出某个产品所有系列在所有店铺中的库存量。为了得到这个结果,我们可以创建一个聚合请求:

 
 db.inventory.aggregate([
  {$match:{productId:”20034”},
  {$unwind:”$vars”},
  {$group:{
      _id:”$result”,
      count:{$sum:”$vars.q”}
    }}
])

在这里,我们从所有店铺中检索某个指定产品的库存,然后使用$unwind操作符将我们的系列数组扩展到一系列文档中,然后再进行分组和求和。这样的操作为我们提供了每个系列的总库存量:

{“_id”: “result”, “count”: 101752}

同样地,我们也可以匹配‘storeId’而不是‘productId’来获得某一个特定店铺所有系列的库存。

使用聚合管道框架,我们可以在库存数据上执行多个不同操作以获得更容易用于报表处理的数据,并获取对信息的真正洞察。听上去还不错?稍等,下面还有更多!

基于位置的库存查询

到现在为止,我们已经从商业的角度主要了解了电商可以从我们的库存系统获得什么,例如追踪和更新库存、生成报表等等,然而该架构最值得注意的优势之一在于其对那些面向用户功能开发的支持。

当我们开始搭建电商参考架构这一部分时,我们了解到库存需要做的不仅仅是在任意给定时间只提供一个库存级别的快照,它也需要支持基于位置的查询类型,特别是在用户的移动和网页应用中。

幸运的是,这对我们的库存系统而言并不是一个问题。由于我们决定将地理位置信息从店铺集合中复制冗余到库存集合中,我们可以非常容易地按照用户位置来检索相关的库存。

回到我们之前使用过用于检索附件商店的 geoNear 命令,所有我们需要做的就是添加一个简单的查询来返回实时信息给顾客,例如查询某一个特定产品在所有用户附近的店铺中可提供的库存:


db.runCommand({

geoNear:”inventory”,

near:{

type:”Point”,

coordinates:[-82.8006,40.0908]},

maxDistance:10000,

spherical:true,

limit:10,

query:{“productId”:”20034”,

“vars.sku”:”sku1736”}})

或者是有他们搜索的这个产品存货的、距离最近的前十个商店:

db.runCommand({

geoNear:”inventory”,

near:{

type:”Point”,

coordinates:[-82.8006,40.0908]},

maxDistance:10000,

spherical:true,

limit:10,

query:{“productId”:”20034”,

“vars”:{

$elemMatch:{”sku1736”, //returns only documents with this sku in the vars array

quantity:{$gt:0}}}}}) //quantity greater than 0

由于我们已经索引了库存文档的‘location’属性,这些查询也会非常高效。这个索引对于要支持用户手机APP的大流量需求是很有必要的。

部署拓扑

现在,可以开始庆祝了,不是吗?我们已经构建了一个极好的、高效的库存系统,能够支持各种各样的查询、更新以及聚合。大功告成!

不要这么快。

该库存系统必须支持一个大型电商的需求,这就意味着它不仅仅要对本地的读写非常高效,也必须能够支撑来自一个庞大地理区域的请求。接下来我们就来讨论一下部署拓扑。

数据中心部署

我们选择在三个数据中心部署我们的库存系统,分别位于美国的西部、中部以及东部地区。然后,我们在相同区域的基础上对数据进行分片,以保证在一个给定区域的所有商店将会在一个单一的本地分片上执行事务,从而最小化跨网延迟。最后,为了保证所有事务,包括那些在其它区域上的库存事务,在本地数据中心上执行,我们在每一个数据中心复制了所有三个分片。

因为我们使用了复制(replication),将会存在一个最终一致性问题:某个区域的用户需要检索属于另一个区域的库存数据。这个库存数据有可能不是实时的。 但是假设数据中心之间的有一个理想的网络连接以及很低的复制延迟情况下,和有较长延迟的跨区域请求相比,使用复制可以降低延迟,是一个值得权衡的一种选择。

片键

当然,在设计任何分片系统时,我们需要仔细考虑使用什么片键。在这种情况下,我们有两个理由选择{storeId:1},{productId:1}。其一,使用‘storeId’确保每个店铺的所有库存都写入到相同分片。第二点,基数。单独使用‘storeId’将会存在问题,因为即使我们有数以百计的店铺,我们将会使用一个相对低基数的片键,一个很明显的问题在于:如果我们处理亿万甚至十亿产品的库存时,将会导致不平衡的集群。解决方案是在片键中将’productId’也包括进去,这就给予了我们想要的基数,我们的库存将会增长到多个分片在每个区域中需要的大小。

分片标签

配置拓扑的最后一步是确保请求被送往本地数据中心的合适分片。为了实现这个功能,我们利用MongoDB中基于标签分片(Tag Aware Sharding)的优点——将一系列分片键值与一个特定的分片或者分片组联系起来。首先,我们为每个区域中的主分片创建一个标签:

- sh.addShardTag(“shard001”,”west”)
- sh.addShardTag(“shard002”,”central”)
- sh.addShardTag(“shard003”,”east”)

然后,将这些标签中的每一个都赋值给相同区域中的一系列商店:

- sh.addTagRange(“inventoryDB.inventory”),{storeId:0},{storeId:100},”west”)
- sh.addTagRange(“inventoryDB.inventory”),{storeId:100},{storeId:200},”central”)
- sh.addTagRange(“inventoryDB.inventory”),{storeId:200},{storeId:300},”east”)

在现实情况下,店铺也许不会整齐地落在每个区域的范围内,但是由于我们可以给任何一个店铺赋予我们想要的任何标签,直到将一个单一的店铺ID赋予到一个标签的级别。这样的做法保证了即使是在店铺ID都不连续的情况下也可以灵活的满足需求。在这里,为了简洁起见,我们在参考架构中只是简单地通过范围来定义了标签的范围。

概括

总的说来,创建库存系统的过程是比较简单的,只需要相对较少的步骤实现。相比于一个完成的系统,更重要的是在这里我们学到了设计这个系统的过程。在MongoDB中,当构建和部署一个满足需求的应用架构时候,你需要在保证高性能和快速响应的同时特别留意模式设计、索引及分片等方面。正如你已经了解的,MongoDB提供了实现这些需求的很多功能。

在接下来的电商参考架构系列的最后一部分中,我们将介绍:可扩展的信息洞察,包括产品推荐及个性化。

了解更多

为了进一步了解如何使用MongoDB重新开启你的零售商之旅,请阅读我们的白皮书。在这篇文章中,你将会了解新的零售挑战以及MongoDB如何解决它们。

为了了解MongoDB的咨询团队如何可以帮助您的应用更快起步,探索我们的开始启动指南。

本文译自:https://www.mongodb.com/blog/post/retail-reference-architecture-part-3-query-optimization-and-scaling

翻译:周颖敏
审稿:TJ

阅读第二部分

快速启动你的应用

发表评论