5

In my application I want to have a live chat feature - in which multiple people (perhaps 5 or more) can chat together at the same time.

I am using a Java based Google App Engine - this is really the first time I've tried to use GAE Datastore, I'm so used to using Oracle/MySQL so I think my strategy is wrong.

Note: For simplicity, I am omitting any validation/security checks In some servlet called WriteMessage I have the following code

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);

In some other servlet called ReadMessages I have the following code

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());

In the javascript client code - the WriteMessage servlet is called any time the user submits a new message - and ReadMessages servlet is called every second to get new messages.

In order to optimize, the javascript will send the id of the last message that it received (or possibly the highest id it has received so far) on subsequent requests to ReadMessage, so that the response only contains messages that it hasn't seen before.

This all seems to work at first, but I think maybe there are a couple of things wrong with this code.

Here is what I think is wrong:

  • Some messages might not be read because I am relying on the id of the ChatMessage's key to filter out messages that the JS client has already seen before - I don't think that will be reliable right?

  • Some writes might fail because there might be 5 or 6 incoming writes at the same exact time - and my understanding is that this might result in ConcurrentModificationException if there are too many writes per second.

  • The date passed on the entity is the current date of the JRE on the application server - maybe I should be using something like "sysdate()" in SQL? I don't know if this is actually an issue or not.

How can I fix the code so that:

  1. All chat messages will be written - would it just be best to have a fail-over so that if the request fails the javascript will just re-attempt until successful?

  2. All chat messages will be read (no exceptions)

  3. Clean up old messages so that only 1000 or so messages are stored

4

1 回答 1

11

当有人在向 SO 发布问题之前实际解决了问题时,这有点令人耳目一新。

虽然您确实列出了您在使用方法时遇到的一堆有效问题,但我建议您最大的问题是成本。您正在为每条聊天消息添加一个新实体,此外,该实体需要被索引。因此,您正在谈论针对发送的每条消息的多个写入操作。您还必须为删除的每个实体付费,因此您必须付费进行清理。

在您的设计的有利方面,您没有使用事务或祖先来创建您的实体,因此您不应该达到写入性能限制。

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

此外,与 SQL 不同,GAE 数据存储区 id 不是单调递增的,因此通过 id GREATER_THAN 进行查询是行不通的。

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

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

  2. 不是查询消息实体,而是通过键获取它们。按键获取实体将为您提供高度一致的结果,而不是最终一致的结果。如果您想确保阅读所有最新的聊天消息,这一点很重要(没有例外)

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

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

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

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

您将需要一个基本上存储 MessageLog 键列表的聊天类型。这允许进行多个聊天。您最初的实现似乎只有一个全局聊天。或者,如果您愿意,只需使用 Chat 的单个实例。

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

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

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

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

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

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

于 2013-05-29T15:10:22.730 回答