GAE数据存储 - 写入多于读取时的最佳实践

时间:2011-04-09 07:37:39

标签: python google-app-engine google-cloud-datastore

我正在尝试使用GAE数据存储区来练习查询和计费机制。

我已经阅读了有关GAE的Oreilly书籍,并观看了有关数据存储的Google视频。我的问题是,最佳实践方法通常涉及更多读取而不是写入数据存储区。

我构建了一个超级简单的应用程序:

  • 有两个网页 - 一个到 选择链接,选择一个视图 链接
  • 每个用户都可以选择将网址添加到他的“链接Feed”
  • 用户可以随时选择任意数量的链接。
  • 在另一个网页上,我想向用户显示他选择的最新10个链接。
  • 每个用户都有自己的“链接Feed”网页。
  • 在我要保存的每个“链接”上并显示一些元数据 - 例如:url链接本身;什么时候被选中;它已经在饲料上出现了多少次;等

在这种情况下,由于用户可以选择他想要的任意数量的链接,只要他想要,我的应用程序写入数据存储区,远远超过读取次数(写入 - 当用户选择另一个链接时;读取 - 当用户打开网页以查看他的“链接提要”)

问题1: 我可以想到(至少)两个选项如何处理这个应用程序的数据:

选项A: - 使用用户详细信息,注册等维护每个用户的实体 - 维护每个用户的另一个实体,该实体持有他最近的10个选定链接,在他要求后将呈现给用户的网页

选项B: - 维护每个网址链接的实体 - 这意味着所有用户的所有网址都将存储为同一个对象 - 维护每个用户的实体细节(与选项A中相同),但在网址的大表中添加对用户网址的引用

什么是更好的方法?

问题2: 如果我想计算到今天所选择的网址总数,或者用户选择的每日网址数量,或任何其他计数 - 我应该将其与我的SDK工具一起使用,还是应该在我上面描述的实体中插入计数器? (我想尽可能多地减少数据存储区写入量)

编辑(回答@Elad的评论): 假设我只想保存每个用户的10个最后网址。其余的我想摆脱(所以不要用不必要的数据来填补我的数据库)。

编辑2:添加代码后 所以我尝试使用以下代码(尝试第一个Elad的方法):

这是我的班级:

class UserChannel(db.Model):
currentUser = db.UserProperty()
userCount = db.IntegerProperty(default=0)
currentList = db.StringListProperty() #holds the last 20-30 urls

然后我序列化了url&元数据到JSON字符串,用户从第一页POST。 这是POST的处理方式:

def post(self):
    user = users.get_current_user()
    if user:  
        logging messages for debugging
        self.response.headers['Content-Type'] = 'text/html'
        #self.response.out.write('<p>the user_id is: %s</p>' % user.user_id())            
        updating the new item that user adds
        current_user = UserChannel.get_by_key_name(user.nickname())
        dataJson = self.request.get('dataJson')
        #self.response.out.write('<p>the dataJson is: %s</p>' % dataJson) 
        current_user.currentPlaylist.append(dataJson)
        sizePlaylist= len(current_user.currentPlaylist)
        self.response.out.write('<p>size of currentplaylist is: %s</p>' % sizePlaylist)
        #whenever the list gets to 30 I cut it to be 20 long
        if sizePlaylist > 30:
            for i in range (0,9):
                current_user.currentPlaylist.pop(i)
        current_user.userCount +=1
        current_user.put()
        Updater().send_update(dataJson) 
    else:
        self.response.headers['Content-Type'] = 'text/html'
        self.response.out.write('user_not_logged_in')

其中Updater是我使用Feed更新Channel-API网页的方法。

现在,一切正常,我可以看到每个用户都有一个包含20-30个链接的ListProperty(当它达到30时,我用pop()将其降低到20),但是!价格相当高...... 每个POST就像这里的那个需要〜200ms,121 cpu_ms,cpm_usd = 0.003588。考虑到我所做的就是将一个字符串保存到列表中,这是非常昂贵的... 我认为问题可能是实体在大型ListProperty中变大了吗?

2 个答案:

答案 0 :(得分:1)

回答1

将链接存储为单独的实体。还为每个用户存储一个实体,其中ListProperty具有最近20个链接的键。当用户选择更多链接时,您只需更新ListProperty键。 ListProperty维护订单,因此只要您遵循FIFO插入订单,就不必担心所选链接的时间顺序。

当您想要显示用户选择的链接(第2页)时,您可以通过一次获取(键)在一次通话中获取所有用户的链接。

回答2

绝对保持计数器,随着实体数量的增加,计数记录的复杂性将继续增加,但对于计数器,性能将保持不变。

答案 1 :(得分:1)

首先,你担心大量写入GAE数据存储区是正确的 - 我自己的经验是它们与读取相比非常昂贵。例如,我的应用程序除了在单个模型表中插入记录之外什么都没做,所以每天只需要几十万次写入,就会耗尽免费配额。因此,处理写入有效地直接转化为您的底线。

第一个问题

我不会将链接存储为单独的实体。数据存储区不是RDBMS,因此标准规范化实践不一定适用。对于每个用户实体,使用ListProperty存储最新的URL及其元数据(您可以将所有内容序列化为字符串)。

  • 由于您只更新单个记录,因此写入效率很高 - 每当用户添加链接时,所有链接记录都不会更新。请记住,为了保持滚动列表(FIFO)的引用URL存储为单独的模型,每个新URL意味着两个写操作 - 插入新URL,删除以删除最旧的URL。
  • 阅读效率也很高,因为用户记录上的一次读取可以为您提供呈现用户Feed所需的所有数据。
  • 从存储角度来看,世界上的网址总数远远超过了您的用户数(即使您成为下一个Facebook),用户选择的网址差异也是如此,因此很可能是URL将只有一个用户 - 在RDBMS样式的数据规范化方面没有真正的好处。

另一个优化想法:如果您的用户通常会在短时间内添加多个链接,您可以尝试批量编写,而不是单独编写。使用memcache存储新添加的用户URL,并使用Task Queue定期将该瞬态数据写入持久数据存储区。我不确定使用任务的资源成本是多少 - 你必须检查。 Here's a good article阅读有关该主题的内容。

第二个问题

使用计数器。请记住,它们在分布式环境中并不是微不足道的,所以请阅读 - 有关于此主题的许多GAE文章,食谱和博客文章 - 只是google appengine counters。在这里,使用memcache应该是一个很好的选择,以减少数据存储区写入的总数。