【问题标题】:JDO - Persisting two entities with same keyJDO - 使用相同的键持久化两个实体
【发布时间】:2012-12-29 23:37:03
【问题描述】:

我正在开发一个 AppEngine 项目,并且我在 AppEngine 数据存储上使用 JDO 来实现持久性。我有一个实体,它使用编码字符串作为键,还使用应用程序生成的键名(也是字符串)。我这样做是因为我的应用程序会经常从野外收集数据(可能会收集相同的东西)并尝试持久化它们。为了避免持久化几个本质上包含相同数据的实体,我决定对这些数据的一些属性进行哈希处理,以获得一致的键名(由于实体关系而不直接操作键)。 现在的问题是,每当我计算我的哈希(键名)并尝试存储实体时,如果它已经存在于数据存储中,那么数据存储(或 JDO 或任何罪魁祸首)会默默地覆盖数据存储中实体的属性,而无需引发任何异常。这会对应用程序产生严重影响,因为它会覆盖实体(我们用于排序)的时间戳(一个字段)。 我怎样才能最好地解决这个问题?

【问题讨论】:

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


    【解决方案1】:

    你需要做get-before-set(检查和设置或CAS)。

    CAS 是并发的基本租户,它是并行计算的必要弊端。

    无论如何,Gets 比 Sets 便宜得多,所以它实际上可以为您省钱。

    不要盲目写入数据存储区,而是先检索;如果实体不存在,则捕获异常并放置实体。如果确实存在,请在保存之前进行深入比较。如果没有任何改变,请不要坚持(并节省成本)。如果它发生了变化,请随意选择合并策略。维护过时修订的一种(略显丑陋)方法是将先前的实体作为字段存储在更新的实体中(可能不适用于许多修订)。

    但是,在这种情况下,您必须在设置之前获取。如果您不希望有很多重复并且想要真正的 chintzy,您可以先执行 exists 查询...这是对您要使用的键执行仅键计数查询(成本比完整获取少 7 倍)。 if (count() == 0) then put() else getAndMaybePut() fi

    count 查询语法可能看起来很慢,但从我的基准测试来看,它是判断实体是否存在的最快(也是最便宜)的方法:

    public boolean exists(Key key){
        Query q;
        if (key.getParent() == null)
          q = new Query(key.getKind());
        else
          q = new Query(key.getKind(), key.getParent());
        q.setKeysOnly();
        q.setFilter(new FilterPredicate(
          Entity.KEY_RESERVED_PROPERTY, FilterOperator.EQUAL, key));
        return 1 == DatastoreServiceFactory.getDatastoreService().prepare(q)
          .countEntities(FetchOptions.Builder.withLimit(1));
    }
    

    【讨论】:

    • (y) 我在想有一种方法可以做到这一点,而无需在潜在的 put 之前进行 get 另外,感谢仅键检索的性能提示。我会实施这个解决方案。
    • 是的。我知道 get-before-set 似乎是一种浪费,但它符合“读取速度快,写入速度慢”的 appengine / keystore 范例。键计数查询速度快得离谱,并且奖励任何可以从已知运行时数据构造键的应用程序。我将使用计数查询的一些示例代码来更新我的答案。
    • 这实际上比“完整获取”花费更多,因为查询花费了一个 read_op 加上一个 small_op。不需要 setKeysOnly(),因为计数查询会自动设置它并且也不返回键。
    • 我认为只有返回对象的查询才需要读取。您不是在查看实体表,而只是检查键表中的数字。如果您返回密钥而不是计算它(更多工作),那将构成一个小操作,而不是读取操作。
    • 嗯。看来你是对的。 developers.google.com/appengine/docs/… 1 读 + 1 小。我想让它付费的唯一方法是检查一组键中的一个是否存在(即使那样,IN 查询的每个项目可能需要 1 次读取 + 1 次小)。虽然 get() 会更便宜,但它可能会更慢。当然,能够异步 get() 意味着您可以立即启动您的请求,然后访问会话或执行其他一些耗时的任务,并在完成后检查 Future,从而使有效 walltime 0millis...
    【解决方案2】:

    在 put() 新实体之前,您必须执行 get() 以查看是否存在具有相同键的实体。没有办法做到这一点。

    您可以使用 memcache 和本地“内存中”缓存来加快 get() 操作。仅当您可能多次阅读相同的信息时,这可能会有所帮助。否则,memcache 查询实际上可能会减慢您的进程。

    为确保两个请求不会相互覆盖,您应该使用事务(不可能使用 Ajax 建议的查询,除非您将所有项目放在一个实体组中,这可能会将您的更新限制为每秒 1 次)

    在伪代码中:

    1. 从散列数据创建密钥
    2. 检查内存缓存中的键(使用 ConcurrentHashSet 键),如果找到则返回
    3. 检查 MemcacheService 的 key,如果找到则返回
    4. 开始交易
    5. 从数据存储中获取实体,如果找到则返回
    6. 在数据存储中创建实体
    7. 提交事务,如果并发更新失败则返回
    8. 将 Key 放入缓存(内存和内存缓存)

    如果另一个请求(线程)已同时写入相同的密钥,则第 7 步将失败。

    【讨论】:

      【解决方案3】:

      我建议您不要将 ID 保存为字符串,而是为您的实体使用 Long ID,或者您可以使用由 appengine 自动生成的 Key 数据类型。

         @PersistenceCapable
         public class Test{
           @PrimaryKey
           @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
           private Long ID;
      
           // getter and setter 
         }
      

      这将每次都向您返回一个唯一值。

      【讨论】:

      • 这种方法的问题是每次唯一的键值都会给我的应用程序提供几个具有相同数据内容的实体。这就是我首先提出散列方法的原因。
      • 你能在这里分享实体代码吗?以便我可以帮助您。
      • 如果您不需要长键,您还需要避免使用它们...数据存储必须锁定每个表的原子长;如果您有数千个并发请求使用长键创建相同类型的实体,您会注意到显着的延迟甚至错误让它们为您生成长键(如果您想手动分配,您可以一次保留一系列键; 这只会锁定一次以获得许多密钥)。这也是为什么你在同一个请求中的长键通常会偏移 1000 的倍数;每个请求都会保留 1000 个密钥,以减少锁定时间。
      • 实际上数据填充是由单个线程以特定时间间隔完成的(一个 cron 作业),我使用的是字符串键,所以我认为我很安全。同时,我已经解决了这个问题,一切正常。感谢您刚刚分享的性能提示,它真的很有启发性!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-11-16
      • 2015-12-02
      • 1970-01-01
      • 2016-06-10
      • 2012-03-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多