电商参考架构第二部分:库存优化方法

在电商参考架构系列的第一部分中,我们介绍了一个大数据量电商如何使用MongoDB作为一个庞大产品目录持久层的一些最佳实践。第一部分中包括了索引、模式以及查询优化以保证我们的目录能够支持类似于搜索、单店价格以及在高效率方式下多方面检索及浏览等特性。在接下来的两篇博客中,我们将介绍相似类型的优化方法,并且将其应用到一个电商业务中完全不同的方面——库存。

一个可以通过电商的店铺及应用访问到的、可靠的、集中的库存系统是提高和丰富用户体验中一个非常庞大的基础部分。下面列举了一个电商或许想要得到的一些特性:

  • 可靠地检查产品的实时库存
  • 提供用户在某个指定实体店提货的选项
  • 在某个商品有促销的情况下,判断每日补给的需求

库存系统的问题

上面这些都是一些看似基础的特性,但是实际上也是目前大多数电商普遍使用的传统库存系统类型所面临的真实问题。在这些系统中,单个店铺维护他们各自的库存,然后在某个特定的时间间隔之后(通常是晚上)将数据返回关系型数据库管理系统中心。接着,关系型数据库管理系统将当天接收到的所有数据整合和分类之后,用于分析、报表等操作,并且将其提供给外部及内部应用。在关系型数据库管理系统和其它应用之间,通常会有一个缓存层,因为在很多情况下,关系型数据库并不是很适合处理该客户端请求的事务数量,特别是面向用户的移动或者网页应用。

因此,现在的问题非常清晰了。这些系统基础的创建并不适用于针对我们拥有多少库存以及库存位置提供一个连续精确的映射关系。此外,还可能带来维护多个系统而导致的复杂性上升的情况,例如:缓存以及持久性等等。而MongoDB则是对这些场景的最好选择 -即使在电商店铺在地理上分布很散,MongoDB仍然可以实现到产品信息的高精确度和系统的高可靠性。

设计原则

首先,我们确定好在电商参考架构中的库存系统应该要做的事情:

  • 提供一个库存的360°视图,可以在任何时间被任何客户端访问
  • 能够被任何需要库存数据的系统使用
  • 解决大数据量、以读取为主的工作负载,例如:库存检查
  • 解决大数据量的实时写操作,例如:库存更新
  • 支持批量写入操作以更新系统记录
  • 地理上分离
  • 伴随着库存中店铺数量或者商品数量的增多,保持水平扩展

简而言之,我们需要的是构建一个高性能、可水平扩展的系统,在一个庞大的、地理分布的区域中的店铺和用户都能够与MongoDB进行实时交互来查看和更新目录。

店铺模式

用户案例的一个基本需求是为每个店铺维护一个关于所有库存的、集中的、实时的视图。我们首先需要为店铺集合创建视图,从而将我们的库存与地理位置相联系起来。结果是:每个店铺都使用一个相当直接的文档。

{

“_id”:ObjectId(“78s89453d8chw28h428f2423”),

“className”:”catalog.Store”,

“storeId”:”store100”,

“name”:”Bessemer Store”,

“address”:{

“addr1”:”1 Main St.”,

“city”:”Bessemer”,

“state”:”AL”,

“zip”:”12345”,

“country”:”USA”

},

“location”:[-86.95444, 33.40178],

}

然后,我们可以创建下列的索引来优化在店铺数据中最经常使用读取类型:

  • {“storeId”:1},{“unique”:true}: 获取某个特定商店的库存
  • {“name”:1}:根据名字获取商店名称
  • {“address.zip”:1}: 获取一个邮编内的所有店铺,例如:店铺定位程序
  • {“location”: 2dsphere}:获取某一个特定地理位置周围的所有商店

在上面所有的索引中,位置索引对我们来说非常有用,因为它允许我们基于某个位置近似查询商店。例如,一个用户寻找某个产品有现货的最近商店。为了在分片环境中利用这个优势,我们使用一条geoNear的命令来检索得到那些“位置”属性在给定点一定距离之内的文档,然后对最近的店铺进行排序:

db.runCommand({
geoNear:“stores”,
near:{
type:”Point”,
coordinates:[-82.8006,40.0908], //GeoJSON or coordinate pair
maxDistance:10000.0, //in meters
spherical:true //required for 2dsphere indexes
}
})

这种模式给了我们定位对象的能力,但是同时也给在这些店铺中追踪和管理库存带来了更大的挑战。

库存数据模型

既然我们已经将商品和店铺联系了起来,我们需要创建一个库存集合来跟踪每一个商品以及它们所有商品系列的真实库存量。然而,我们需要在其中进行一定的取舍。为了同时最小化对数据库的来回读取数目,同时降低应用级的连接,我们决定将数据从店铺集合复制到库存集合。我们提出的文档是这样的:

{
“_id”:”902372093572409542jbf42r2f2432”,
“storeId”:”store100”,
“location”:[-86.95444, 33.40178],
“productId”:”20034”,
“vars”:[
{“sku”:”sku1”, “quantity”:”5”},
{“sku”:”sku2”, “quantity”:”23”},
{“sku”:”sku3”, “quantity”:”2”},

]
}

首先注意到:我们在库存文档中同时包括了’storeId’和’location’属性。很明显,‘storeId’对于我们知道哪个商店有什么商品是非常必要的,但是——当我们查询离用户附近的库存时会发生什么呢?需要同时使用到库存数据以及店铺位置数据才能完成这个请求。通过在库存文档中添加地理位置数据,我们消除了在店铺集合中执行一个单独查询的需求,也减少了店铺集合和库存集合的一个连接操作。

此外,在我们的模式中,我们还决定在商品级别文档中表示库存。正如我们在电商参考架构系列第一部分中提到的,每个产品可能会拥有成百上千的商品系列/型号,包括尺寸、颜色、风格等等,所有这些系列必须在我们的库存中表示出来。那么,问题就是:我们是否应该支持包含一个更大系列集合的更大文档,还是在具体商品型号上表示库存的更多文档呢?在这种情况下,我们比较倾向于更大的文档以降低数据冗余度,这样做也可以减少在库存集合中需要查询或者更新的文档总数。

接下来,我们创建索引:

  • {storeId:1}: 得到某一个指定商店库存中的所有商品
  • {productId:1},{storeId:1}: 获取一个指定店铺中某个产品的库存
  • {productId:1},{location:”2dsphere”}:获取在一定距离之内的某个产品的所有库存

值得注意的是:我们并没有选择创建一个包含’ vars.sku’ 的索引。没有这样做的原因是:这并不会给我们带来非常多的好处,因为我们已经可以基于’productID’查询我们的库存了:

db.inventory.find(
{
“storeId”:”store100”,
“productId”:“20034”,
“vars.sku”:”sku11736”
},
{“vars.$”:1}
)

实际上,我们并不会从’ vars.sku’索引上受益多少。在这种情况下,在’productID’上的索引已经可以得到文档了,因此在该变量上的索引是不必要的。此外,由于系列数组有可能有成千上万的条目,在上面的索引可能会占用一大块内存,从而减少在内存中存储的文档数目,这就意味着会降低查询速度。考虑所有的事情,在给定目标的前提下,这是一个不中意的取舍。

那么是什么使得我们的模式如此合适呢?我们将在下一篇博客中讨论这个方法为库存系统提供的一些特色。

了解更多

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

本文译自:https://www.mongodb.com/blog/post/retail-reference-architecture-part-2-approaches-inventory-optimization

翻译:周颖敏
审稿:TJ

阅读第一部分

快速启动你的应用

发表评论