0

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

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

我有一个限制:服务器是无状态的!它没有关于客户端的会话信息。

我目前的想法如下:

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

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

  1. 将 create-tag 存储为条目内的索引属性:
    • 当服务器收到请求时,它必须使用查询来查找任何现有条目
    • 问题:由于 AppEngine 中的查询只有最终一致,它可能会丢失一个条目
  2. 使用 create-tag 作为条目的键:
    • 应该没问题,因为如果数字不换行,它保证是唯一的(不太可能是多头)
    • 小不便:它会在将来的任何使用中增加条目密钥的长度(不必要的开销)
    • 主要问题:这将在数据存储中生成顺序条目键,应不惜一切代价避免这种情况,因为它会在存储的数据中创建热点,从而显着影响性能

对于选项 2,我正在考虑的一种解决方案是使用某种公式,该公式采用序列号并将它们重新映射到一个唯一的、确定的但看起来随机的序列上,而不是消除热点。关于这样的公式可能是什么样子的任何想法?

或者也许有更好的方法?

4

2 回答 2

1

如何为新实体分配密钥?

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

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

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

于 2014-09-22T19:38:52.897 回答
0

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

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

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

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

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

有一些方法可以处理这些冲突。例如:如果发生碰撞,比较实际内容,检查是否确实是重复的,如果不是,则在键中添加“1”。然后,查看该密钥是否也存在,如果存在,则再次检查它是否具有相同的内容。如果不是,则添加一个“2”,再次检查是否有碰撞,依此类推......虽然这可行,但它变得非常混乱。

或者你可以说哈希冲突是如此罕见,以至于一个人的数据库中永远不会有足够的用户数据来查看一个。我个人不喜欢这种“保持手指交叉”的方法,但在许多情况下,这可能是一种可以接受的方式。

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

使用与原始问题相同的标识符,我的创建标签SI.PL.RISI组成,它将永远增加,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 个键字节以相反顺序构建的整数,以将键分布在存储桶中。

于 2014-09-23T17:42:23.003 回答