在本地GAE HRD上,或者可能是重用事务中,已提交的JDO写入不起作用。

27
我正在应用JDO 2.3在App Engine上进行开发。之前我用了主/从数据存储来进行本地测试,最近我转而使用HRD数据存储进行本地测试,这导致我的应用程序中的某些部分出现问题(这是可以预料的)。其中一个出现问题的部分是当它快速发送大量写入时,由于1秒限制,会出现并发修改异常。
好的,这也是可以预料的,所以我让浏览器稍后重试写操作(也许不是最好的方法,但我只是想快速让它工作)。
但是奇怪的事情发生了。有些应该成功的写请求(那些没有出现并发修改异常的请求)也会失败,尽管提交阶段已完成且请求返回成功代码。我可以从日志中看到重新尝试的请求正常工作,但是这些其他请求似乎在第一次尝试提交时已经“提交”了,但是我猜可能从未“应用”。但是根据我所读到的关于应用阶段的内容,再次对该实体进行写操作应该会强制执行应用…但它没有这样做。
以下是相关代码。请注意以下几点:
  1. 我正在尝试使用自动JDO缓存。因此,JDO在内部使用了memcache。如果没有将所有内容都包装在事务中,则这实际上不起作用。
  2. 所有请求只是从实体中读取一个字符串,修改字符串的一部分,然后将该字符串保存回实体中。如果这些请求不在事务中,则当然会出现“脏读”问题。但是在事务中,隔离级别应该是“可序列化”的,所以我不知道这里发生了什么。
  3. 正在修改的实体是根实体(不在组中)
  4. 已启用跨组事务
以下是相关代码(这是简化版本):
PersistenceManager pm = PMF.getManager();
Transaction tx = pm.currentTransaction();
String responsetext = "";
try {
    tx.begin();
    // I have extra calls to "makePersistent" because I found that relying
    // on pm.close didn't always write the objects to cache, maybe that
    // was only a DataNucleus 1.x issue though
    Key userkey = obtainUserKeyFromCookie();
    User u = pm.getObjectById(User.class, userkey);
    pm.makePersistent(u); // to make sure it gets cached for next time
    Key mapkey = obtainMapKeyFromQueryString();
    // this is NOT a java.util.Map, just FYI
    Map currentmap = pm.getObjectById(Map.class, mapkey);
    Text mapData = currentmap.getMapData(); // mapData is JSON stored in the entity
    Text newMapData = parseModifyAndReturn(mapData); // transform the map
    currentmap.setMapData(newMapData); // mutate the Map object
    pm.makePersistent(currentmap); // make sure to persist so there is a cache hit
    tx.commit();
    responsetext = "OK";
} catch (JDOCanRetryException jdoe) {
    // log jdoe
    responsetext = "RETRY";
} catch (Exception e) {
    // log e
    responsetext = "ERROR";
} finally {
    if (tx.isActive()) {
        tx.rollback();
    }
    pm.close();
}
resp.getWriter().println(responsetext);

更新:我很确定我知道为什么会发生这种情况,但我仍然会奖励给任何能够证实的人。

基本上,我认为问题在于事务并没有真正地在本地版本的数据存储中得到执行。参考资料:

https://groups.google.com/forum/?fromgroups=#!topic/google-appengine-java/gVMS1dFSpcU https://groups.google.com/forum/?fromgroups=#!topic/google-appengine-java/deGasFdIO-M https://groups.google.com/forum/?hl=en&fromgroups=#!msg/google-appengine-java/4YuNb6TVD6I/gSttMmHYwo0J

因为事务没有被实现,回滚实际上是一个无操作。因此,在两个事务同时尝试修改记录时,我得到了脏读取。换句话说,在同一时间内,A读取数据和B读取数据。A试图修改数据,B试图修改不同部分的数据。 A写入数据存储区,然后B写入,抹去A的更改。然后app引擎"回滚" B,但由于在本地数据存储上运行时回滚是一个无操作,因此B的更改保留,而A的更改则不保留。同时,由于B是抛出异常的线程,客户端重新尝试B,但不会重试A(因为A据说是成功的事务)。


你是否考虑重新设计你的数据存储和使用方式,以避免每秒钟将相同实体组持久化超过一次?或者,你是否尝试将数据存储持久化交给排队任务,并安排事物以遵守每秒钟1次实体组写入频率限制? - Ian Marshall
我已经考虑过这个问题。但在此之前,我想先了解为什么会出现这个特定的错误...我的担忧是,我可能根本不理解HRD或应用程序引擎/JDO事务等方面的某些内容,或者我在文档中漏掉了某些东西,这将在以后给我带来麻烦,因为我还需要添加至少25个其他服务的事务(如果数据存储访问不在事务中,则JDO缓存无法工作)。 - eeeeaaii
就目前插件(GAE JDO v2.x)而言,我认为没有必要在事务中访问L2缓存;如果读取了一个对象,则它将被L2缓存,如果没有读取,则应该报告(显然旧插件不受支持,因此只有在当前插件下才能报告这样的情况)。 - DataNucleus
2
@DataNucleus升级到新插件,得到了相同的行为。我不理解的是,代码执行了两次数据存储读取,然后进行了一次写入操作。当启用缓存时,你会认为这两次读取都应该来自缓存,所以唯一需要访问数据存储的就只有写入操作了。但事实并非如此。相反,唯一计费的交易是一次数据存储读取,没有数据存储写入发生。为什么? - eeeeaaii
嗨@DataNucleus...我添加了“执行数据存储操作”的代码,对此感到抱歉。非常感谢您在此问题上给予的任何见解。该页面一遍又一遍地修改同一个映射,因此在第一次请求之后,如果被缓存,则应该从缓存中获取。使用DataNucleus 2.0版本,我不必在jdoconfig中指定额外的缓存内容,它会自动缓存(但您已经知道了)。基本上,如果没有事务,由于某种原因它不会从缓存中获取,当它正在缓存时,它只会执行单个读取而不是写入(每个appstat),这对我来说毫无意义。 - eeeeaaii
显示剩余6条评论
1个回答

1
也许对你来说是个坏消息,我离开了JDO,现在使用Objectify,在某些地方直接使用datanucleus。我对持久化有完全的控制,这是一个更好的性能和设计选择(如果你考虑长期的话)。
因为数据库是非关系型的,所以与JPA、JDO和标准假设相比存在结构上的变化:
使用本机datanucleus API可以做一些标准JPA甚至Objectify中没有的事情:我使用的例子是动态创建列。
在GAE中不存在事务,有时会出现类似事务的东西(实体组)。因此,使用本机API将避免您进行不可预测的体操。
尝试用操纵杆驾驶汽车可能有效,但肯定需要学习新技能。在我看来,值得学习本机方式。

1
Zied 是正确的。虽然 Google 提供了 JPA 和 JDO 抽象层,这可能会引起轻微的兴趣,但我从未见过一个真正成功地使用它们的项目。通常会出现各种各样的丑陋的黑客攻击,因为人们一直把后备数据存储库视为 RDBMS,而它肯定不是。我强烈建议人们不要使用 JPA 或 JDO 库,而要使用原始的 Google API 或 Objectify。 - Jeff Richley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接