从数据存储加载数据集并合并为单一字典。资源问题

1 投票
3 回答
512 浏览
提问于 2025-04-15 22:34

我有一个产品数据库,里面包含了产品、零件和每个零件的标签,这些标签是根据语言代码来区分的。

我遇到的问题是,获取不同的数据集并把它们合并成一个字典所消耗的资源太大了,导致效率低下。

数据库中的产品是由一些特定类型的零件组成的(比如颜色、大小)。每个零件都有对应语言的标签。为此我创建了四个不同的模型:产品、产品零件、产品零件类型和产品零件标签。

我把问题缩小到大约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 个回答

0

我觉得问题出在设计上:想要在memcache中构建一个关系连接表,而这个框架是特别不推荐这样做的。

GAE会把你的任务扔掉,因为它花的时间太长,但其实你根本就不应该这样做。我自己也是GAE的新手,所以很遗憾我不能具体说应该怎么做。

1

一个重要的事情是要知道,使用 IN 查询(还有 != 查询)时,后台会生成多个子查询,而且最多只能有30个子查询。

所以,当你写 ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts) 这个查询时,实际上后台会生成 len(self.defaultParts) 个子查询。如果 len(self.defaultParts) 超过30个,就会出错。

这里有一段来自 GQL 参考文档 的相关内容:

注意: IN!= 操作符在后台会使用多个查询。例如,IN 操作符会为列表中的每个项目执行一个单独的数据库查询。返回的结果是所有这些查询的交叉结果,并且会去重。任何单个 GQL 查询最多只能有30个数据库查询。

你可以尝试为你的应用安装 AppStats,看看还有哪些地方可能导致性能变慢。

2

这里有几个简单的想法:

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)

撰写回答