如何使用Google App Engine数据存储(HRE)创建可靠的聊天?

时间:2013-05-29 05:42:26

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

在我的应用程序中,我希望有一个实时聊天功能 - 多个人(可能是5个或更多)可以同时聊天。

我正在使用基于Java的Google App Engine - 这是我第一次尝试使用GAE数据存储区,我已经习惯使用Oracle / MySQL,所以我认为我的策略是错误的。

注意:为简单起见,我省略了任何验证/安全检查 在一些名为WriteMessage的servlet中,我有以下代码

Entity entity = new Entity("ChatMessage");
entity.setProperty("userName", request.getParameter("userName"));
entity.setProperty("message", request.getParameter("message"));
entity.setProperty("time", new Date());
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(entity);

在一些名为ReadMessages的servlet中,我有以下代码

String id = request.getParameter("id");
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Query query = new Query("ChatMessage");
if (id != null) {
  // Client requested only messages with id greater than this id
  Filter idFilter = new FilterPredicate(Entity.KEY_RESERVED_PROPERTY,
        FilterOperator.GREATER_THAN,
        KeyFactory.createKey("ChatMessage", Long.parseLong(id)));
  query.setFilter(idFilter);
}
PreparedQuery pq = datastore.prepare(query);
JsonArray messages = new JsonArray();
for (Entity result : pq.asIterable()) {
  JsonObject jmsg = new JsonObject();

  // Client will use this id on the next request to read to poll only
  // "new" messages
  jmsg.addProperty("id", result.getKey().getId());
  jmsg.addProperty("userName", (String) result.getProperty("userName"));
  jmsg.addProperty("message", (String) result.getProperty("message"));
  jmsg.addProperty("time", ((Date) result.getProperty("time")).getTime());
  messages.add(jmsg);
}
PrintWriter out = response.getWriter();
out.print(messages.toString());

在javascript客户端代码中 - 每次用户提交新消息时都会调用WriteMessage servlet - 每秒调用ReadMessages servlet以获取新消息。

为了优化,javascript将在ReadMessage的后续请求中发送它收到的最后一条消息的ID(或者可能是迄今为止收到的最高ID),以便响应仅包含消息它以前没见过。

这一切似乎最初起作用,但我想这个代码可能有一些问题。

我觉得这是错误的:

  • 有些消息可能无法读取,因为我依赖于ChatMessage密钥的id来过滤掉JS客户端之前已经看过的消息 - 我认为这不可靠吗?

  • 某些写操作可能会失败,因为在同一时间可能会有5或6个传入写入 - 我的理解是,如果每秒写入次数过多,则可能会导致ConcurrentModificationException

  • 在实体上传递的日期是应用程序服务器上JRE的当前日期 - 也许我应该在SQL中使用类似“sysdate()”的东西?我不知道这实际上是不是一个问题。

如何修复代码以便:

  1. 所有聊天消息都将被写入 - 是否最好进行故障转移,以便在请求失败时javascript只会重新尝试直到成功?

  2. 将阅读所有聊天消息(无例外)

  3. 清理旧邮件,以便只存储1000条左右的邮件

1 个答案:

答案 0 :(得分:11)

当某人在向SO发布问题之前实际处理过问题时,它会有点令人耳目一新。

虽然您确实列出了一系列与您的方法相关的有效问题,但我建议您最大的问题是费用。您正在为每个聊天消息添加新实体,此外该实体需要编制索引。因此,您正在谈论发送的每条消息的多个写操作。您还必须为您删除的每个实体付费,因此您必须付清单才能清理。

在您的设计的正面,您不使用交易或祖先来创建您的实体,因此您不应该达到写入性能限制。

在阅读方面,您为每条消息读取了一个实体,因此成本也会增加。您在没有事务或祖先查询的情况下查询这一事实意味着您在查询时可能看不到最新的ChatMessage实体。

此外,与SQL不同,GAE数据存储区ID不会单调增加,因此通过id GREATER_THAN查询将无法正常工作。

现在提出建议。我警告你,这将是很多工作。

  1. 尽量减少您使用的实体数量。不是为每个消息添加新实体,而是使用一个更大的实体,每个实体存储多条消息。

  2. 不是查询消息实体,而是按键获取它们。按键获取实体将为您提供强烈一致的结果,而不是最终一致的结果。如果您想确保读取所有最新的聊天消息(无例外)

  3. ,这一点很重要

    这确实引入了您需要处理的两个新问题:

    • 如果多次写入到同一实体,您将达到某种写入性能限制。

    • 由于您的实体可能会变得很大,因此您需要处理此案例,以确保它们不会超过1MB的限制。

    您需要两个实体种类。您需要一个存储多条消息的MessageLog种类。您可能希望将消息存储为MessageLog中的List。对于给定的聊天,您将需要多个MessageLog实体,主要用于写入性能。 (搜索" Google App Engine Sharding"了解更多信息)。

    您需要一种基本上存储MessageLog密钥列表的聊天类型。这允许多个聊天继续。您的原始实现似乎只有一个全局聊天。或者如果你想要,只需使用聊天的单个实例。

    这些都不需要编入索引,因为您将通过Key获取所有内容。这将降低成本。

    当您开始新的聊天时,您将根据您期望的需要创建一些MessageLog实体。每期每秒写入1个实体。如果聊天中有更多人,我会创建更多MessageLog。然后创建一个Chat实体并在其中存储MessageLog键列表。

    在写入消息时,您将执行以下操作: - 按键获取相应的聊天实体,现在有一个MessageLog列表 - 选择一个MessageLog来分配负载,这样所有写入都不会命中同一个实体。选择一个可能有多种技术,但是对于这个例子,随机选择一个。 - 格式化新消息并将其插入MessageLog。您也可以考虑在此时删除MessageLog中的旧消息。您还需要进行一些安全检查,以确保MessageLog在1MB实体大小限制内。 - 编写MessageLog。这应该只产生1个写操作而不是写入新实体的最少3个写操作。 推荐:将消息附加到包含整个聊天记录的给定聊天的memcache条目。

    在阅读中,您将执行以下操作: 推荐:首先检查给定聊天的memcache条目,如果存在,则返回完成。 - 按键获取相应的聊天实体,现在有一个MessageLog列表 - 按键获取所有MessageLog。现在,您在聊天中收到了所有消息,并且它们是最新的。 - 解析所有MessageLog,并重建整个聊天记录。 建议:将重建的消息日志存储在内存缓存中,这样您就不必再次执行此操作。 - 返回重建的聊天记录。

    考虑使用Channel API将消息发送给查看者。观看者可以通过这种方式比每秒更快地接收消息。我个人发现Channel API并不是100%可靠,所以我不会完全摆脱轮询,但你可能每隔30秒轮询一次作为备份。

    想象一下聊天,其中包含100条消息。您的原始计划将花费大约101个读取操作读取100条消息。在这种新方法中,你有5-10个MessageLog实体,因此成本将是6-11个读取操作。如果你得到一个memcache命中,你就不需要任何读操作。但您必须编写代码以从多个MessageLog对象重建聊天日志。

相关问题