我在Datastore中有一个看起来像这样的实体:
public class UserEntry {
@Parent
private Ref<User> parent;
@Id
private String id;
private String seqNumber;
private String name;
}
我正在尝试维护每个用户的序列号。即用户的第一个条目的seqNumber应为1,下一个为2,依此类推。实现这一目标的最佳方法是什么?
即: 1)如何获得用户最后一个条目的seqNumber
2)我如何确保在写入时另一个进程没有为具有相同seqNumber的用户写入条目。我无法使seqNumber成为条目的id。
答案 0 :(得分:2)
我担心实现此目的的唯一方法是使用数据存储区对事务的支持。但请注意,此解决方案存在相当大的争用风险。如果操作不正确,可能会跳过序列中的某些值。让我从一个简单的方法开始,来说明基本的想法。
直接解决方案(NAIVE APPROACH):
您可以创建一个专用实体,让我们称之为Sequence
,它将拥有一个属性,让我们称之为value
。开始时,value
属性将包含0
(或1
,具体取决于您希望序列的起始位置。然后,在创建任何新UserEntry
之前,您必须执行以下事务:
value
,value
增加一(在同一交易中)。您将使用事务的事实将阻止并发请求获取相同的顺序ID。但请注意,必须有一个&#34;实例&#34;存储在数据存储区中的Sequence
实体类型。过快地更新此实体可能会导致争用问题。此外,此方法使用非幂等事务,这可能导致从序列中跳过某些值。
争用风险:
请注意,上述直接解决方案会限制应用程序的吞吐量。您的应用程序无法在较长时间内处理每秒创建多个UserEntry
的应用程序。这是因为创建UserEntry
需要更新Sequence
实体。并且&#34;每秒写一次&#34;是写入单个实体的近似限制,请参阅https://cloud.google.com/datastore/docs/concepts/limits
非幂等交易的危险:
数据存储可能偶尔抛出一个错误,声称事务失败,即使它没有,请参阅https://cloud.google.com/datastore/docs/concepts/transactions如果您在此类&#34;非错误&#34;之后重试该事务,您将最终执行交易两次。在您的场景中,您最终会增加value
两次以创建单个UserEntry
,从而跳过序列中的一个值(如果您非常不幸并且得到&#34;非更多值) -error&#34;连续好几次。)
这就是为什么谷歌建议你的交易是幂等的,这意味着执行交易一千次应该对基础数据的结果状态产生与执行一次相同的效果。幂等事务的一个很好的例子是重命名用户。如果你告诉别人改名为&#34; Carl&#34;一千次,他最终会被称为......好吧,#34; Carl&#34;。另一方面,如果你告诉我们的value
计数器增加了一千倍......你得到了图片。
更好的解决方案:
如果你对上述直接解决方案的风险感到满意,你可以去。但是这里有一些提示如何避免这些问题:
避免争用:
您可以使用任务队列推迟seqNumber
的分配。通过确保队列不会每秒发送多次请求,您可以轻松避免可能的争用问题。此解决方案的明显缺点是,在将seqNumber
属性分配给新创建的UserEntry
之前会有一些延迟。我不知道这对你是否可以接受。
设计交易是幂等的:
这是一个简单的修改,它会使事务具有幂等性:使用它来存储最后创建的value
的{{1}},而不是使用id
属性来保存实际的计数器值。 。然后,在确定下一个UserEntry
的{{1}}应该是什么时,检索最后添加的seqNumber
,使用其UserEntry
计算下一个值,然后更新{ {1}}实体(根据您的需要多次)告诉它,您的UserEntry
属性现在等于&#34;某个特定的ID&#34;。
最后说明:
在使用seqNumber
作为实体的ID时,你是非常正确的。使用单调递增值作为实体ID是另一个众所周知的争用陷阱,请参阅https://cloud.google.com/datastore/docs/best-practices
希望这有帮助。