2

我不确定方法的正确设计。

我们使用放置在每个实体上的递增版本来使用乐观锁定。long这种实体的每次更新都是通过比较和交换算法执行的,该算法成功或失败取决于其他客户端是否同时更新实体。经典的乐观锁定,例如hibernate。

我们还需要采取重试的方式。我们使用http基于存储(etcd),并且可能会发生某些更新请求刚刚超时。

这就是问题所在。如何结合乐观锁和重试。这是我面临的具体问题。

假设我有一个实体,version=1我正在尝试更新它。下个版本显然是2. 我的客户比执行条件更新。只有当持久化中的版本是1并且它被原子地更新为version=2. 到目前为止,一切都很好。

现在,假设更新请求的响应没有到达。目前还不能说成功与否。我现在唯一能做的就是重试更新。在内存实体中仍然包含version=1打算将值更新为2.

现在真正的问题出现了。如果第二次更新因为持久化版本 is2而不是而失败1怎么办?

有两个可能的原因:

  1. 第一个请求确实导致了更新 - 操作成功但响应丢失或我的客户端超时,无论如何。它只是没有到达,但它通过了
  2. 其他一些客户端在后台同时执行更新

现在我不能说什么是真的。我的客户更新了实体还是其他客户做了?操作是通过还是失败?

我们当前使用的方法只是比较持久实体和主内存中的实体。作为 java 相等或 json 内容相等。如果它们相等,则更新方法被声明为成功。我对算法不满意,因为它对我来说既不便宜也不合理。

另一种可能的方法是不使用long版本,timestamp而是使用。每个客户端都会在更新操作中生成自己的时间戳,这意味着潜在的并发客户端很可能会生成其他客户端。我的问题是概率,特别是当两个并发更新来自同一台机器时。

还有其他解决方案吗?

4

2 回答 2

1

您可以使用两步协议在 etcd 中伪造交易。

更新算法:

第一阶段:记录更新到 etcd

  • 添加一个具有相当小的 TTL 的“更新锁定”节点。如果存在,请等待它消失,然后重试。
  • 在您的代码中添加一个看门狗。如果执行后续步骤的时间超过锁的 TTL(或者如果您未能刷新它),您必须中止。
  • 添加具有 [old,new] 值的“更新计划”节点。它的结构取决于您,但您需要确保在持有锁时复制旧值。
  • 添加一个“提交更新”节点。此时您已经“原子地”更新了数据。

第二阶段:执行实际更新

  • 阅读“计划更新”节点并应用它描述的更改。
    • 如果更改失败,请验证新值是否存在。
    • 如果不是,你有一个大问题。保释。
  • 删除提交更新节点
  • 删除更新计划节点
  • 删除更新锁节点

如果要读取一致的数据:

  • 虽然没有提交更新节点,但您的数据还可以。
  • 否则,等待它被删除。

    • 每当提交更新存在但更新锁定不存在时,启动恢复。

事务恢复,如果你发现一个没有锁的更新计划节点:

  • 获取更新锁。
  • 如果没有提交更新节点,则删除计划并释放锁。
  • 否则,继续上面的“第二阶段”。
于 2015-11-26T15:03:57.573 回答
0

恕我直言,因为etcd它是基于 HTTP 构建的,HTTP 本质上是一个不安全的协议,所以很难有一个防弹解决方案。

经典 SQL 数据库使用连接的协议、事务和日志记录,以允许用户确保整个事务将被完全提交或完全回滚,即使在操作中间断电的最坏情况下也是如此。

因此,如果 2 个操作相互依赖(资金从一个银行账户转移到另一个),您可以确保两者都正常或都没有,您可以简单地在数据库中实现一个“操作”日志,其状态为即使您在提交过程中断开连接,也可以稍后通过咨询期刊来查看是否通过了特定的测试。

但我根本无法想象这样的解决方案etcd。所以除非别人找到更好的方法,否则你有两个选择

  • 在后端使用经典的SQL 数据库,使用etcd(或等效)作为简单缓存
  • 接受协议的弱点

顺便说一句,我不认为用时间戳代替长版本号会加强系统,因为在高负载下,两个客户端事务使用相同时间戳的概率会增加。也许您可以尝试在您的字段中添加一个唯一的 id(客户端 id 或只是技术 uuid),当版本为 n+1 时,只需比较增加它的 UUID:如果是您的,则交易通过,如果不是 id 没有。

但是,如果您现在可以阅读该版本,则会出现更严重的问题,它不是在 n+1 处,而是已经在 n+2 处。如果 UUID 是你的,你肯定你的交易通过了,但如果不是没有人可以说。

于 2015-06-12T10:19:51.027 回答