从数据存储加载数据集并合并为单一字典。资源问题
我有一个产品数据库,里面包含了产品、零件和每个零件的标签,这些标签是根据语言代码来区分的。
我遇到的问题是,获取不同的数据集并把它们合并成一个字典所消耗的资源太大了,导致效率低下。
数据库中的产品是由一些特定类型的零件组成的(比如颜色、大小)。每个零件都有对应语言的标签。为此我创建了四个不同的模型:产品、产品零件、产品零件类型和产品零件标签。
我把问题缩小到大约10行代码,这些代码似乎是导致问题的根源。目前我有3个产品、3种类型、每种类型3个零件,以及2种语言。结果请求的处理时间竟然达到了5500毫秒。
for product in productData:
productDict = {}
typeDict = {}
productDict['productName'] = product.name
cache_key = 'productparts_%s' % (slugify(product.key()))
partData = memcache.get(cache_key)
if not partData:
for type in typeData:
typeDict[type.typeId] = { 'default' : '', 'optional' : [] }
## Start of problem lines ##
for defaultPart in product.defaultPartsData:
for label in labelsForLangCode:
if label.key() in defaultPart.partLabelList:
typeDict[defaultPart.type.typeId]['default'] = label.partLangLabel
for optionalPart in product.optionalPartsData:
for label in labelsForLangCode:
if label.key() in optionalPart.partLabelList:
typeDict[optionalPart.type.typeId]['optional'].append(label.partLangLabel)
## end problem lines ##
memcache.add(cache_key, typeDict, 500)
partData = memcache.get(cache_key)
productDict['parts'] = partData
productList.append(productDict)
我猜问题出在循环的次数太多了,需要反复遍历相同的数据。labelForLangCode会从ProductPartLabels中获取与当前语言代码匹配的所有标签。
每个产品的所有零件都存储在一个db.ListProperty(db.key)中,零件的所有标签也是如此。
我需要这个有点复杂的字典是因为我想展示一个产品的所有数据,包括它的默认零件,并且显示一个选择器来选择可选零件。
defaultPartsData和optionalPartsData是产品模型中的属性,结构如下:
@property
def defaultPartsData(self):
return ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts)
@property
def optionalPartsData(self):
return ProductParts.gql('WHERE __key__ IN :key', key = self.optionalParts)
当完成的字典在内存缓存中时,运行得很顺畅,但如果应用进入休眠状态,内存缓存会被重置吗?我还想在第一次用户访问页面时(内存缓存为空)展示内容,而不想有那么大的延迟。
正如我之前所说,这只是少量的零件和产品。如果有30个产品,每个产品有100个零件,结果会怎样呢?
一个解决方案是创建一个定时任务,每小时将数据缓存到内存中?这样有效吗?
我知道这信息量很大,但我现在卡住了。我已经连续研究了大约12个小时,还是找不到解决办法。
..fredrik
编辑:
这是一个AppStats的截图,在这里。
根据我在AppStats中看到的,查询似乎没问题,只需200-400毫秒。为什么差距会这么大呢?
编辑2:
我实现了dound的解决方案,并做了一些改进。现在看起来是这样的:
langCode = 'en'
typeData = Products.ProductPartTypes.all()
productData = Products.Product.all()
labelsForLangCode = Products.ProductPartLabels.gql('WHERE partLangCode = :langCode', langCode = langCode)
productList = []
label_cache_key = 'productpartslabels_%s' % (slugify(langCode))
labelData = memcache.get(label_cache_key)
if labelData is None:
langDict = {}
for langLabel in labelsForLangCode:
langDict[str(langLabel.key())] = langLabel.partLangLabel
memcache.add(label_cache_key, langDict, 500)
labelData = memcache.get(label_cache_key)
GQL_PARTS_BY_PRODUCT = Products.ProductParts.gql('WHERE products = :1')
for product in productData:
productDict = {}
typeDict = {}
productDict['productName'] = product.name
cache_key = 'productparts_%s' % (slugify(product.key()))
partData = memcache.get(cache_key)
if partData is None:
for type in typeData:
typeDict[type.typeId] = { 'default' : '', 'optional' : [] }
GQL_PARTS_BY_PRODUCT.bind(product)
parts = GQL_PARTS_BY_PRODUCT.fetch(1000)
for part in parts:
for lb in part.partLabelList:
if str(lb) in labelData:
label = labelData[str(lb)]
break
if part.key() in product.defaultParts:
typeDict[part.type.typeId]['default'] = label
elif part.key() in product.optionalParts:
typeDict[part.type.typeId]['optional'].append(label)
memcache.add(cache_key, typeDict, 500)
partData = memcache.get(cache_key)
productDict['parts'] = partData
productList.append(productDict)
结果好多了。没有内存缓存时大约3000毫秒,有内存缓存时大约700毫秒。
我还是有点担心3000毫秒的问题,在本地的app_dev服务器上,每次重新加载内存缓存都会被填满。难道不应该把所有东西都放进去,然后再从中读取吗?
最后,有人知道为什么在生产服务器上请求的时间比在app_dev上长大约10倍吗?
编辑3:
我注意到没有一个db.Model是被索引的,这会有影响吗?
编辑4:
在咨询AppStats后(理解它花了一些时间),似乎大问题出在part.type.typeId上,其中part.type是一个db.ReferenceProperty。我之前应该注意到这一点,也许我可以更好地解释一下 :) 我会重新考虑那部分,并再跟你们说。
..fredrik
3 个回答
我觉得问题出在设计上:想要在memcache中构建一个关系连接表,而这个框架是特别不推荐这样做的。
GAE会把你的任务扔掉,因为它花的时间太长,但其实你根本就不应该这样做。我自己也是GAE的新手,所以很遗憾我不能具体说应该怎么做。
一个重要的事情是要知道,使用 IN
查询(还有 !=
查询)时,后台会生成多个子查询,而且最多只能有30个子查询。
所以,当你写 ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts)
这个查询时,实际上后台会生成 len(self.defaultParts)
个子查询。如果 len(self.defaultParts)
超过30个,就会出错。
这里有一段来自 GQL 参考文档 的相关内容:
注意:
IN
和!=
操作符在后台会使用多个查询。例如,IN
操作符会为列表中的每个项目执行一个单独的数据库查询。返回的结果是所有这些查询的交叉结果,并且会去重。任何单个 GQL 查询最多只能有30个数据库查询。
你可以尝试为你的应用安装 AppStats,看看还有哪些地方可能导致性能变慢。
这里有几个简单的想法:
1) 既然你需要获取所有的结果,不如直接调用fetch()来一次性获取所有数据,而不是像你现在这样用for循环。因为用for循环的话,可能会导致多次查询数据存储,因为每次只能获取有限的项目。比如,你可以试试这样:
return ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts).fetch(1000)
2) 也许在最开始的请求中只加载一部分数据。然后可以用AJAX技术根据需要加载更多数据。比如,先返回产品信息,然后再发起额外的AJAX请求来获取配件。
3) 正如Will所指出的,IN
查询对于每个参数都会执行一次查询。
- 问题: 一个IN查询会对你提供的每个参数执行一次等于查询。所以
key IN self.defaultParts
实际上会执行len(self.defaultParts)
次查询。 - 可能的改进: 尝试对你的数据进行更好的去规范化。具体来说,在每个配件上存储一个它所用到的产品列表。你可以这样设计你的Parts模型:
class ProductParts(db.Model): ... products = db.ListProperty(db.Key) # product keys ...
- 这样你就可以对每个产品执行一次查询,而不是对每个产品执行N次查询。比如,你可以这样做:
parts = ProductParts.all().filter("products =", product).fetch(1000)
- 权衡是什么? 你需要在每个ProductParts实体中存储更多的数据。此外,当你写入一个ProductParts实体时,会稍微慢一点,因为这会导致你的列表属性中的每个元素在索引中写入一行。不过,你提到你只有100个产品,所以即使一个配件被所有产品使用,列表也不会太大(Nick Johnson提到过这里,你在尝试索引一个大约5,000个项目的列表属性之前不会遇到问题)。
不那么关键的改进建议:
4) 你可以创建GqlQuery对象一次,然后重复使用。这并不是你主要的性能问题,但会稍微有帮助。例子:
GQL_PROD_PART_BY_KEYS = ProductParts.gql('WHERE __key__ IN :1')
@property
def defaultPartsData(self):
return GQL_PROD_PART_BY_KEYS.bind(self.defaultParts)
你还应该使用AppStats,这样你就能准确看到请求为什么会花这么长时间。你甚至可以考虑在你的帖子中附上appstats关于你请求的截图。
如果你重写代码以减少与数据存储的往返次数,代码可能会是这样的(这些改动基于上面提到的想法#1、#3和#4)。
GQL_PARTS_BY_PRODUCT = ProductParts.gql('WHERE products = :1')
for product in productData:
productDict = {}
typeDict = {}
productDict['productName'] = product.name
cache_key = 'productparts_%s' % (slugify(product.key()))
partData = memcache.get(cache_key)
if not partData:
for type in typeData:
typeDict[type.typeId] = { 'default' : '', 'optional' : [] }
# here's a new approach that does just ONE datastore query (for each product)
GQL_PARTS_BY_PRODUCT.bind(product)
parts = GQL_PARTS_BY_PRODUCT.fetch(1000)
for part in parts:
if part.key() in self.defaultParts:
part_type = 'default'
else:
part_type = 'optional'
for label in labelsForLangCode:
if label.key() in defaultPart.partLabelList:
typeDict[defaultPart.type.typeId][part_type] = label.partLangLabel
# (end new code)
memcache.add(cache_key, typeDict, 500)
partData = memcache.get(cache_key)
productDict['parts'] = partData
productList.append(productDict)