在AppEngine中进行幂等的客户端可重试条目创建,最终给出一致的查询等

时间:2014-09-22 19:18:13

标签: google-app-engine eventual-consistency idempotent

我需要提出一个策略来处理创建数据存储条目的客户端重试:

  • 客户端发送请求以在数据库中创建新条目
  • 服务器执行条目创建并准备成功回复
  • 发生了一些错误,使客户认为请求未得到处理(数据包丢失,......)
  • 客户端再次发送相同的请求以在数据库中创建新条目
  • 服务器检测到重试并重新创建并发送原始回复,而不创建其他数据存储条目
  • 客户收到回复
  • 每个人都很开心,只在数据库中创建了一个条目

我有一个限制:服务器是STATELESS!它在客户端没有任何会话信息。

我目前的想法如下:

  • 使用保证的全局唯一ID标记每个创建请求(这里是我如何创建它们,尽管它们与问题不太相关):
    • 使用数据存储(和内存缓存),我会在加载后为每个服务器实例分配一个唯一的,单调递增的ID(让我们称之为 SI
    • 当客户端请求起始页时,提供请求的实例会生成唯一的单调递增页面加载ID( PL )并发送 SI.PL 与客户一起连接页面内容
    • 对于每个创建请求,客户端生成一个唯一的单调递增请求ID( RI )并发送 SI.PL.RI 以及create-request < / LI>
  • 对于每个创建请求,服务器首先检查它是否知道create-tag
  • 如果没有,它会创建新条目并以某种方式存储create-tag及其
  • 如果它确实知道标签,它会使用它来查找最初创建的条目并重新创建相应的回复

以下是我现在正在考虑的实施方案及其问题:

  1. 将create-tag存储为条目内的索引属性:
    • 当服务器收到请求时,它必须使用查询来查找任何现有条目
    • 问题:由于AppEngine中的查询最终只是一致,因此可能会错过条目
  2. 使用create-tag作为条目的键:
    • 应该没问题,因为如果数字不包裹(不太可能有长片),保证是唯一的
    • 轻微的不便:它增加了条目的长度&#39;任何将来使用的密钥(不需要的开销)
    • 主要问题:这将在数据存储中生成顺序输入密钥,应该不惜一切代价避免它creates hot-spots in the stored data,因此会显着影响性能
  3. 我正在考虑选项2的一个解决方案是使用某种公式来获取序列号并将它们重新映射到一个独特的,确定的但随机的序列,而不是消除热点。关于这样一个公式可能是什么样的想法?

    或许可能有一个更好的方法?

2 个答案:

答案 0 :(得分:1)

如何为新实体分配密钥?

如果您自己创建密钥,问题就解决了。重复实体将简单地覆盖现有实体,因为它具有相同的密钥。一个例子是创建一个产品实体,其中产品的SKU用于生成密钥。

如果数据存储分配了密钥,则当请求超时时,向用户显示错误消息并将数据重新加载到客户端。然后,用户将查看是否已创建实体。

它不像“随机序列”那样花哨,但它更简单,更可靠:)

答案 1 :(得分:0)

虽然可以从上面的工作中创建实现选项(1),但通过使用设计巧妙的数据结构,正确的事务和垃圾收集,它将是相当的让它可靠地工作是一种痛苦。

因此,似乎正确的解决方案是使用选项(2)的通用版本:

为创建的实体使用一些唯一标识符作为实体的密钥。

这样,如果在重试中再次创建相同的实体,则可以很容易地可靠地找到现有副本(因为gets非常一致),或者盲目地再次写入它只会覆盖第一个版本

@Greg 在评论中建议使用哈希来实体唯一标识数据作为密钥。虽然这解决了在参数空间中均匀分布密钥并因此导致跨物理存储位置有效分布数据的问题,但它确实产生了必须管理(或忽略)哈希冲突的新问题,尤其是如果一个人试图让密钥变得很长。

有办法处理这些碰撞。例如:如果发生碰撞,请比较实际内容以检查它是否真的重复,如果没有,则添加&#34; 1&#34;关键。然后,查看该密钥是否也存在,如果存在,再次检查它是否具有相同的内容。如果没有,请添加&#34; 2&#34;相反,再次检查碰撞,等等......虽然这有效,但它会变得非常混乱。

或者你可以说哈希冲突是如此罕见,以至于人们永远不会在一个数据库中拥有足够的用户数据。我个人不喜欢这些&#34;保持你的手指交叉&#34; -approaches,但在许多情况下,它可能是一种可接受的方式。

但是,幸运的是,我已经为数据创建了一个无冲突的全局唯一标识符:create-tag。事实证明,我使用它看到的两个问题都可以通过一些聪明的比特改组来轻松解决:

使用与原始问题相同的标识符,我的create-tag SI.PL.RI SI 组成,它将永远增加, PL ,每次创建新服务器实例时重置为0, RI ,重置每个新客户端会话。所以 RI 可能总是很小, PL 会保持一点点,而 SI 会慢慢变大。

鉴于此,我可以像这样构建密钥(从最重要的位开始):

- Lowest 10 bits of PL
- Lowest  4 bits of RI
- Lowest 17 bits of SI
- 1 bit indicating whether there are any further non-zero values
- Next lowest 10 bits of PL
- Next lowest  4 bits of RI
- Next lowest 17 bits of SI
- 1 bit indicating whether there are any further non-zero values
- ... until ALL bits of RI, PL, and SI are used (eventually breaking 10-4-17 pattern)

这样,如果按词汇顺序排序(如AppEngine那样),生成的键会在参数空间中很好地传播,并且第一个键只有自动生成的键的一半,并且它们只会根据需要变长

除了1:

实际上,如果没有服务器实例存活的时间足以支持超过一千个页面加载,并且没有一个客户端在一个会话中创建超过16个新实体,并且服务器实例不会更快地生成平均每5分钟就会超过一个,平均值超过4个字节需要一年多的时间。

如果没有服务器实例活着的时间超过百万页面加载,并且没有一个客户端在一个会话中创建超过256个新实体,并且服务器实例产生的平均速度不会高于每第二一个实例,在密钥长度超过8个字节(因此长于自动生成的字节)之前,它仍需要500多年的时间。应该没问题...... :)

除了2:

如果我需要使用密钥来索引Java HashMap,我的密钥对象的hashCode()函数可以返回一个从前4个密钥字节构建的整数。反向命令将密钥分散到桶中。