点击上方“大数据猫”,每日干货!

目前暂时更新两部分内容:
1.搭建一个灵活、可搜索、响应快速的产品目录系统
2.库存优化
第一部分
搭建一个灵活、可搜索、响应快速的产品目录系统
如今,产品目录数据管理对零售商而言是一个非常复杂的问题。经过多年对多个庞大、由供应商提供的系统的依赖之后,零售商目前正在重新考虑他们的选择,并且开始展望未来。
在如今供应商提供的系统中,产品数据必须得频繁地使用 ETL 工具来回迁移,以保证所有的系统均在相同数据集上进行操作。这个方法就开发和管理而言是非常缓慢、容易出错,并且非常昂贵的。因此,零售商目前正在努力将数据服务单独作为一个集中的、面向服务架构(SOA)的一部分。
这是我们在MongoDB中通常看到的一个模式,因此我们开始定义一些最佳实践以及专门面向于电商空间的参考架构。作为该成果的一部分,今天我们将开始介绍如何使用MongoDB实现一项目录服务,并将其作为在零售商架构系列(共三部分)的第一部分。
为什么选择MongoDB?
许多不同的数据库类型都可以实现我们的产品目录用户案例,那么为什么要选择MongoDB呢?
文档灵活性:每个MongoDB文档都可以将数据存储为丰富的JSON结构。这就使得MongoDB对于存储任何对象都非常理想,包括拥有每个商品都有成千上万系列的庞大目录。
动态的模式:每个文档中的JSON结构可以随时进行调整,保证了需要修改时数据的灵活性以及易重构性。在MongoDB中,这些多重模式可以存储于一个单一的集合中,也可以使用共享索引,保证了新、旧格式的同步高效搜索。
有表现力的查询语言:能够在多个文档属性之间进行查询的能力简化了许多任务。这也可以通过减少数据库必要请求次数来提高应用的性能。
索引:MongoDB从一开始就提供了强大的二级、复合及地理索引选项,保证了像排序以及基于位置的查询之类的特色。
数据一致性:默认地,所有的读写操作都会被送到一个MongoDB复制集的主节点上。这样就保证了强一致性——一个对零售商而言非常重要的特性。因为他们可能会有许多顾客对相同的商品目录进行多次请求。
地理分布的复制集:由数据源与用户之间的地理距离带来的网络延迟是一个难题,尤其对于一个期望维持大量低延迟读取的目录服务而言。MongoDB的复制集可以是地理上分离的,因此它们距离用户非常近,在很多情况下可以保证快速存取、减轻内容分发网络的需求。
这些只是MongoDB成为对电商而言很好的选择的一些特性。接下来,我们将介绍一下如何将其中的一部分特性运用于我们的零售商参考架构,来支持许多特色,包括:
对商品及商品系列的搜索
对商品在每个店铺价格的检索
允许目录的多方面搜索和浏览
商品数据模型
我们需要考虑的第一件事就是商品的数据模型。在下面的例子中,我们只展示了对每件商品而言最重要的信息,例如类别、品牌以及描述:
{
“_id”: “30671”, //main item ID
“department”: “Shoes”,
“category”: “Shoes/Women/Pumps”,
“brand”: “Calvin Klein”,
“thumbnail”: “http://cdn.../pump.jpg”,
“title”: “Evening Platform Pumps”,
“description”: “Perfect for a casual night out or a formal event.”,
“style”: “Designer”,
…
}
这种简单的数据模型允许我们非常容易基于最重要原则对商品进行查询。例如,使用db.collection.findOne
,将会返回一个满足一个查询的单一文档:
通过ID得到商品:
db.definition.findOne({_id:”301671”})通过一系列产品ID得到商品:
db.definition.findOne({_id:{$in:[”301671”,”452318”]}})通过类别前缀得到商品:
db.definition.findOne({category:/^Shoes\/Women/})
注意第二个和第三个查询分别是如何使用$in操作符以及一个正则表达式的。当在正确索引的文档中执行这些类型的查询时,MongoDB可以为这些类型的查询提供高吞吐量以及低延迟的能力。
系列数据模型
对产品目录而言另一个重要的考量是商品系列,例如现有尺寸、颜色以及风格。上述的数据模型只能获取到关于每个目录商品一小部分的数据。因此,对于所有现有的、我们也许需要检索的商品系列(例如大小和颜色)而言又该怎么处理呢?
一个选择是在一个单一文档中存储一个商品以及它所有的系列。这种方法拥有能够在一个单一查询中检索一个商品以及其所有系列的优点。然而,它并不是在所有情况下都是最好的方法。避免无限制的文档增长是一个非常重要的最佳实践。如果产品系列的数据以及它们相关数据非常小,在商品文档中存储这些数据也许会有意义。
另一个选择是创建一个能够关联到主商品的、单独的系列数据模型:
{
“_id”: ”93284847362823”, //variant sku
“itemId”: “30671”, //references the main item
“size”: 6.0,
“color”: “red”
…
}
这个数据模型允许我们通过它们的商品编号来快速检索到特定的商品系列:
db.variation.find({_id:”93284847362823”})
也可以通过对itemId 属性的查询获得某个特定商品的所有系列:
db.variation.find({itemId:”30671”}).sort({_id:1})
通过这个方法,我们同时维护了在目录中展示主商品以及当用户请求一个更详细的产品视图时对每个系列的快速查询。我们也可以保证商品以及系列文档的一个可预测大小。
不同店铺不同价格
在定义产品目录的参考架构时另一个考虑是价格。我们已经看到了一些方法,能够结构化我们的商品,以直接或基于特定属性快速检索商品。价格有可能受很多因素影响,例如店铺的位置。我们需要一个方法快速检索出任何一个给定商品或者商品系列的特定价格。这对于大型零售商而言是非常困难的,因为一个拥有一百万商品以及一千个商店的商品目录意味着我们必须在一个十亿文档集合中进行查询以获得任意一个给定商品的价格。
当然,我们也可以将每个系列的价格作为一个嵌套文档在商品文档中存储起来,但是一个更好的解决方法是再次利用MongoDB可以对_id 进行快速查询的优点。例如,如果产品目录中每个商品都被一个商品ID引用,同时它的每个系列都被一个商品编号(SKU)索引,那么我们就可以将每个文档的_id设置为商品ID或者商品编号(SKU)的一个级联,并且将商店ID与价格变量相关联。通过使用这个模型,上面提到的每双单鞋的_id以及它的红色种类应该看起来是这样的:
商品:
30671_store23某个特定规格的商品:
93284847362823_store23
这种方法也为处理价格提供很大的灵活性,因为它允许我们在商品或者系列级别对商品进行定价。我们可以查询所有价格或者只是某个特定店铺的价格:
所有价格:
db.prices.find({_id:/^30671/})某个特定店铺的价格:
db.prices.find({_id:/^30671_store23/})
我们甚至可以添加其他组合,例如每个店铺群的价格,然后在单个查询中使用$in操作符获取对于一个商品而言所有可能的价格:
db.prices.find({_id:{$in:[ “30671_store23”,
“30671_sgroup12”,
“93284847362823_store23”,
“93284847362823_sgroup12” ]}})
浏览和搜索商品
对我们的产品目录而言,最大的一个挑战就是能够提供多方面的搜索和浏览。尽管许多用户想要使用某个特定商品或者他们正在寻找的条件来搜索我们的产品目录,但是更多的其他用户想要的是浏览,然后通过一系列属性来限制返回结果。因此,给定创建一个像下面这个页面一样的需求:
我们有许多的挑战:
响应时间:在用户浏览的同时,结果的每个页面应该在毫秒内返回。
多个属性:伴随着用户选择不同的方面(例如,品牌、大小、颜色等),新的查询必须能够在多个文档属性中运行。
系列级别属性:一些用户选择的属性将会在商品级别进行查询,例如品牌,但是其它的查询则有可能运行于系列级别上,例如尺寸。
多个系列:每个商品都有可能有成千上万个系列,但是我们只希望每个商品只展示一次,因此,结果必须消除重复项。
排序:用户需要能够在多个属性上进行排序,例如价格、尺寸,此外排序操作必须能够高效运行。
分页:每个页面只返回少量结果,这就要求确定性排序。
许多零售商也许会想要使用一个专用的搜索引擎作为这些特色的基础。MongoDB就提供了一个开源的连接件项目,它允许MongoDB和Apache Solr 以及Elasticsearch同时使用。然而,对于我们的参考架构,我们想完全在MongoDB中实现一个多方面搜索。
{
“_id”: “30671”,
“title”: “Evening Platform Pumps”,
“department”: “Shoes”,
“Category”: “Women/Shoes/Pumps”,
“price”: 149.95,
“attrs”: [“brand”: “Calvin Klein”, …],
“sattrs”: [“style”: ”Designer”, …],
“vars”: [
{
“sku”: “93284847362823”,
“attrs”: [{“size”: 6.0}, {“color”: “red”}, …],
“sattrs”: [{“width”: 8.0}, {“heelHeight”: 5.0}, …],
}, … Many more SKUs
]
}
为了实现这个功能,我们创建了另一个集合,用于存储所谓的摘要文档。这些文档包含了我们需要基于多个搜索方面对产品目录中商品进行快速检索的所有信息。
注意:在这个数据模型中,我们定义了属性以及辅助属性。尽管一个用户也许会希望能够在某个商品或者商品系列的许多不同属性上进行搜索,但是我们只会保存一个最经常使用的核心集合。例如,给定一双鞋,对于一个用户而言,基于现有尺寸大小的查询会比基于后跟高度查询更普遍。通过在我们的数据模型中同时使用attr和sattr属性,我们可以将所有商品属性提供给搜索,但是也会带来只索引最经常使用的属性attr花费的提高。
通过使用这个数据模型,我们可以创建以下复合索引:
部门+属性+类别+ _id
部门+变量属性+类别+ _id
部门+类别+ _id
部门+价格+ _id
部门+评分+ _id
在这些目录中,我们经常从部门开始,然后我们假设用户将会选择部门来重新定义他们的搜索结果。对于没有部门的一个产品目录,我们可以非常轻易地从另一个像类别或者类型等比较普遍的方面开始。然后,我们可以执行需要进行多方面搜索的查询,并且快速将结果返回到页面:
从商品ID获取摘要
db.variation.find({_id:”30671”})获取特定商品系列的摘要
db.variation.find({vars.sku:”93284847362823”},{“vars.$”:1})通过部门获取所有商品的摘要
db.variation.find({department:”Shoes”})使用一系列混合的参数获取摘要
db.variation.find({ department”:”Shoes”,“vars.attr”: {“color”:”red”},“category”: “^/Shoes/Women”})
概要重述
今天我们了解了一些多功能商品目录系统的建模和索引的最佳实践,包括商品及商品系列的查询、店铺价格以及支持多样化搜索的目录浏览。使用这些方法作为一个起点,将会帮助你找到对于你自己的项目而言最好的设计。
第二部分
库存优化
在电商参考架构系列的第一部分中,我们介绍了一个大数据量电商如何使用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
上的索引已经可以得到文档了,因此在该变量上的索引是不必要的。此外,由于系列数组有可能有成千上万的条目,在上面的索引可能会占用一大块内存,从而减少在内存中存储的文档数目,这就意味着会降低查询速度。考虑所有的事情,在给定目标的前提下,这是一个不中意的取舍。
那么是什么使得我们的模式如此合适呢?我们将在下一篇博客中讨论这个方法为库存系统提供的一些特色。




