【问题标题】:GAE datastore - best practice when there are more writes than readsGAE 数据存储 - 写入多于读取时的最佳实践
【发布时间】:2011-04-09 07:37:39
【问题描述】:

我正在尝试对 GAE 数据存储进行一些练习,以了解查询和计费机制。

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

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

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

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

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

选项 A: - 使用用户详细信息、注册等维护每个用户的实体 - 为每个用户维护另一个实体,保存他最近选择的 10 个链接,在他请求后将其呈现到用户的网页

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

什么是更好的方法?

问题 2: 如果我想计算直到今天选择的 url 总数,或者用户选择的每日 url 数量,或者任何其他计算 - 我应该将它与我的 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 的处理方式:

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 是我使用 Channel-API 更新带有提要的网页的方法。

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

【问题讨论】:

  • 从您的问题中不清楚您是要永久保存用户选择的所有 URL,还是只保存 20 个最近的 URL 并丢弃旧的。你能澄清一下吗?
  • 你是对的。这是两种不同的情况。我用你评论的内容更新了我的问题。
  • 太棒了 - 这让它更简单 - 请参阅下面的答案。顺便说一句,出于纯粹的好奇心,应用程序是什么?为什么我要保存一些 URL,但只能看到最后 10 个?
  • @Elad - 谢谢。 10 只是一个数字,它可以是任何数字,但我正在考虑一个动态提要,比如我的 facebook,只是我真的不需要超过 x 天的数据,因为它不相关。当然,将来我可能想永远保存用户的提要历史记录,但不是现在。

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


【解决方案1】:

首先,您担心对 GAE 数据存储的大量写入是正确的 - 我自己的经验是,与读取相比,它们非常昂贵。例如,我的一个应用程序只在单个模型表中插入记录,达到了每天几十万次写入的免费配额。因此,有效地处理写入直接转化为您的底线。

第一个问题

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

  • 这对于写入非常有效,因为您只更新一条记录 - 每当用户添加链接时,所有链接记录都不会更新。请记住,要保留一个滚动列表 (FIFO),其中引用 URL 存储为单独的模型,每个新 URL 都意味着两个写入操作 - 插入新 URL,以及删除最旧的 URL。
  • 读取也很有效,因为对用户记录的单次读取即可为您提供呈现用户提要所需的所有数据。
  • 从存储的角度来看,全世界的 URL 总数远远超过你的用户数(即使你成为下一个 Facebook),你的用户选择的 URL 的方差也是如此,所以很可能平均值URL 将只有一个用户 - 在 RDBMS 风格的数据规范化方面没有真正的收获。

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

第二个问题

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

【讨论】:

  • 感谢@Elas!好吧,当您说“将所有内容序列化为字符串”时,您的意思是像 - 例如 - youtube url,其中许多参数位于 URL 中?很多 = & 和 ?区分不同的参数?
  • 将所有内容序列化为字符串可以避免 2 次写入,但请记住,如果您以后想要存储任何带有链接的属性,例如共享它的用户数量,那么您将无法搜索和排序您的属性,您将不得不反序列化列表属性元素以找到属性的值。
  • @Hamutsi 问题是 ListProperty 不能存储任意对象,所以如果你想存储除了 URL 之外的信息,你需要以某种方式对其进行序列化。 URL 参数是一个有趣的想法。我更多地考虑 JSON。
  • @Yasser 你是对的,当然存储 blob 的缺点是不允许你搜索它们。然而,现在的主要要求是最少的写入次数。我相信总的来说,最好根据现有需求进行设计,而不是潜在的未来需求——那些往往不会实现。如果他们这样做了,那么,当你到达它时,你会过桥。
  • 我的意思是每个用户有一个 ListProperty,该列表中有 20 个元素,每个元素都包含 URL + 元数据。 ListProperty 元素必须以某种方式序列化 - ListProperty 不会采用任意对象,JSON 是一种序列化选项。
【解决方案2】:

回答 1

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

当您想要显示用户选择的链接(第 2 页)时,您可以执行一次 get(keys) 以在一次调用中获取所有用户的链接。

答案 2

一定要保留计数器,随着实体数量的增长,计数记录的复杂度会不断增加,但是有了计数器,性能会保持不变。

【讨论】:

  • 谢谢亚瑟。我正在考虑带有 ref 键的 ListProperty,但这是我的恐惧 - 现在对于每个链接选择,数据存储区都有两次写入:1-添加新的链接实体; 2-更新用户实体中最近更新的链接列表。假设用户不断选择链接,是不是很贵?
  • 如果您为每个链接创建一个实体并将用户作为参考属性保留在其中,那么随着用户和链接数量的增加,查找用户最近的 20 个链接的成本将不断增加。您可以通过链接实体中的附加时间戳来降低该成本,并在用户和时间戳上建立一个复合索引以加快查找速度。
  • Yasser,当您写道“如果您为每个链接创建一个实体并将用户作为参考属性保留在其中”时,您的意思是而不是您之前建议的内容,或者是附加的(引用到的 ListProperty键)
  • 我最初的建议是,即您应该保留一个列表属性,但是正如您所指出的那样,这需要两次写入数据存储。我在评论中建议的是,如果您想避免写入,那么一种方法是保留参考属性,即在 GAE 中进行 1-m 关系的标准方法。
猜你喜欢
  • 2011-09-23
  • 2019-07-21
  • 2015-10-16
  • 1970-01-01
  • 2013-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-21
相关资源
最近更新 更多