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

3 投票
2 回答
785 浏览
提问于 2025-04-16 15:23

我正在尝试练习使用GAE(Google App Engine)数据存储,以便了解查询和计费机制。

我读过关于GAE的O'Reilly书籍,也看过Google关于数据存储的视频。我的问题是,最佳实践通常更关注读取数据而不是写入数据。

我建立了一个超级简单的应用:

  • 有两个网页 - 一个用来选择链接,另一个用来查看选择的链接。
  • 每个用户可以选择将网址链接添加到他的“链接订阅”中。
  • 用户可以随时选择任意数量的链接。
  • 在另一个网页上,我想展示用户最近选择的10个链接。
  • 每个用户都有自己独特的“链接订阅”网页。
  • 在每个“链接”上,我想保存并显示一些元数据 - 比如:链接本身;选择的时间;它在订阅中出现的次数等等。

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

问题 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

然后我将网址和元数据序列化为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是我用Channel-API更新网页订阅的方法。

现在,一切都正常,我可以看到每个用户都有一个包含20-30个链接的ListProperty(当达到30个时,我用pop()将其减少到20),但是!价格相当高……

每个像这样的POST大约需要200毫秒,121 cpu_ms,cpm_usd= 0.003588。考虑到我所做的只是将一个字符串保存到列表中,这非常昂贵……我觉得问题可能是因为大ListProperty导致实体变得很大?

2 个回答

1

回答 1

把链接单独存储成不同的实体。同时,为每个用户存一个实体,里面有一个列表属性,记录最近的20个链接。当用户选择更多链接时,只需要更新这个列表属性里的键就可以了。列表属性会保持顺序,所以你不需要担心链接的选择顺序,只要按照先进先出的方式插入就行。

当你想展示用户选择的链接(比如在第二页)时,可以一次性通过一个获取操作把所有用户的链接都取出来。

回答 2

一定要保持计数器,因为随着实体数量的增加,记录的计数复杂度会不断上升,但有了计数器,性能就能保持稳定。

1

首先,你担心在GAE数据存储上进行大量写入是对的——根据我的经验,这些写入的成本比读取要高得多。举个例子,我有一个应用程序,光是往一个模型表里插入记录,每天就写入了几万条,结果很快就用完了免费的配额。所以,如何高效地处理写入操作,直接影响到你的成本。

第一个问题

我建议不要把链接单独存储为不同的实体。数据存储不是关系型数据库,所以标准的规范化做法不一定适用。对于每个用户实体,可以使用一个列表属性来存储最近的URL和它们的相关信息(你可以把所有内容序列化成一个字符串)。

  • 这样写入效率高,因为你只需要更新一条记录——当用户添加链接时,不需要更新所有的链接记录。要记住,如果你想保持一个滚动列表(先进先出),而把URL存储为单独的模型,每添加一个新URL就需要进行两次写入操作——一次是插入新URL,另一次是删除最旧的那个。
  • 读取时也很高效,因为只需读取用户记录,就能获取到渲染用户动态所需的所有数据。
  • 从存储的角度来看,世界上URL的总数远远超过你的用户数量(即使你成为下一个Facebook),而且用户选择的URL种类也非常多,所以很可能平均每个URL只有一个用户——在数据的关系型数据库风格规范化上并没有实际的好处。

另一个优化建议是:如果你的用户通常在短时间内添加多个链接,可以尝试批量写入,而不是单独写入。可以使用memcache来存储新添加的用户URL,然后利用任务队列定期将这些临时数据写入持久数据存储。不过,我不太确定使用任务的资源成本——你需要自己查一下。这里有一篇不错的文章可以了解这个主题。

第二个问题

使用计数器。只要记住,在分布式环境中,计数器并不是简单的东西,所以要多了解一下——有很多关于GAE的文章、食谱和博客帖子可以参考——只需在网上搜索appengine计数器。在这里,使用memcache也是一个不错的选择,可以减少数据存储的总写入次数。

撰写回答